From 5f2cc327671cc7974dcb0b8ffabdc07b82cce3f0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 15 Jan 2025 07:39:03 +0100 Subject: [PATCH] PICARD-1861: Remove old plugin options page --- picard/ui/options/dialog.py | 1 - picard/ui/options/plugins.py | 732 ----------------------------------- 2 files changed, 733 deletions(-) delete mode 100644 picard/ui/options/plugins.py diff --git a/picard/ui/options/dialog.py b/picard/ui/options/dialog.py index bc193ec2b1..99f643a9ff 100644 --- a/picard/ui/options/dialog.py +++ b/picard/ui/options/dialog.py @@ -83,7 +83,6 @@ matching, metadata, network, - plugins, profiles, ratings, releases, diff --git a/picard/ui/options/plugins.py b/picard/ui/options/plugins.py deleted file mode 100644 index ecb70e61eb..0000000000 --- a/picard/ui/options/plugins.py +++ /dev/null @@ -1,732 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Picard, the next-generation MusicBrainz tagger -# -# Copyright (C) 2007 Lukáš Lalinský -# Copyright (C) 2009 Carlin Mangar -# Copyright (C) 2009, 2018-2023 Philipp Wolfer -# Copyright (C) 2011-2013 Michael Wiencek -# Copyright (C) 2013, 2015, 2018-2024 Laurent Monin -# Copyright (C) 2013, 2017 Sophist-UK -# Copyright (C) 2014 Shadab Zafar -# Copyright (C) 2015, 2017 Wieland Hoffmann -# Copyright (C) 2016-2018 Sambhav Kothari -# Copyright (C) 2017 Suhas -# Copyright (C) 2018 Vishal Choudhary -# Copyright (C) 2018 yagyanshbhatia -# Copyright (C) 2023 tuspar -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - - -from functools import partial -from html import escape -from operator import attrgetter -import os.path -import re - -from PyQt6 import ( - QtCore, - QtGui, - QtWidgets, -) -from PyQt6.QtWidgets import QTreeWidgetItemIterator - -from picard import log -from picard.config import get_config -from picard.const import ( - PLUGINS_API, - USER_PLUGIN_DIR, -) -from picard.extension_points.options_pages import register_options_page -from picard.i18n import ( - N_, - gettext as _, -) -from picard.util import ( - icontheme, - open_local_path, - reconnect, -) - -from picard.ui import HashableTreeWidgetItem -from picard.ui.forms.ui_options_plugins import Ui_PluginsOptionsPage -from picard.ui.options import OptionsPage -from picard.ui.theme import theme -from picard.ui.util import FileDialog - - -COLUMN_NAME, COLUMN_VERSION, COLUMN_ACTIONS = range(3) - - -class PluginActionButton(QtWidgets.QToolButton): - - def __init__(self, icon=None, tooltip=None, retain_space=False, - switch_method=None, parent=None): - super().__init__(parent=parent) - if tooltip is not None: - self.setToolTip(tooltip) - - if icon is not None: - self.setIcon(self, icon) - - if retain_space is True: - sp_retain = self.sizePolicy() - sp_retain.setRetainSizeWhenHidden(True) - self.setSizePolicy(sp_retain) - if switch_method is not None: - self.switch_method = switch_method - - def mode(self, mode): - if self.switch_method is not None: - self.switch_method(self, mode) - - def setIcon(self, icon): - super().setIcon(icon) - # Workaround for Qt sometimes not updating the icon. - # See https://tickets.metabrainz.org/browse/PICARD-1647 - self.repaint() - - -class PluginTreeWidgetItem(HashableTreeWidgetItem): - - def __init__(self, icons, *args, **kwargs): - super().__init__(*args, **kwargs) - self._icons = icons - self._sortData = {} - self.upgrade_to_version = None - self.new_version = None - self.is_enabled = False - self.is_installed = False - self.installed_font = None - self.enabled_font = None - self.available_font = None - - self.buttons = {} - self.buttons_widget = QtWidgets.QWidget() - - layout = QtWidgets.QHBoxLayout() - layout.setContentsMargins(0, 2, 5, 2) - layout.addStretch(1) - self.buttons_widget.setLayout(layout) - - def add_button(name, method): - button = PluginActionButton(switch_method=method) - layout.addWidget(button) - self.buttons[name] = button - button.mode('hide') - - add_button('update', self.show_update) - add_button('uninstall', self.show_uninstall) - add_button('enable', self.show_enable) - add_button('install', self.show_install) - - self.treeWidget().setItemWidget(self, COLUMN_ACTIONS, - self.buttons_widget) - - def show_install(self, button, mode): - if mode == 'hide': - button.hide() - else: - button.show() - button.setToolTip(_("Download and install plugin")) - button.setIcon(self._icons['download']) - - def show_update(self, button, mode): - if mode == 'hide': - button.hide() - else: - button.show() - button.setToolTip(_("Download and upgrade plugin to version %s") % self.new_version.short_str()) - button.setIcon(self._icons['update']) - - def show_enable(self, button, mode): - if mode == 'enabled': - button.show() - button.setToolTip(_("Enabled")) - button.setIcon(self._icons['enabled']) - elif mode == 'disabled': - button.show() - button.setToolTip(_("Disabled")) - button.setIcon(self._icons['disabled']) - else: - button.hide() - - def show_uninstall(self, button, mode): - if mode == 'hide': - button.hide() - else: - button.show() - button.setToolTip(_("Uninstall plugin")) - button.setIcon(self._icons['uninstall']) - - def save_state(self): - return { - 'is_enabled': self.is_enabled, - 'upgrade_to_version': self.upgrade_to_version, - 'new_version': self.new_version, - 'is_installed': self.is_installed, - } - - def restore_state(self, states): - self.upgrade_to_version = states['upgrade_to_version'] - self.new_version = states['new_version'] - self.is_enabled = states['is_enabled'] - self.is_installed = states['is_installed'] - - def __lt__(self, other): - if not isinstance(other, PluginTreeWidgetItem): - return super().__lt__(other) - - tree = self.treeWidget() - if not tree: - column = 0 - else: - column = tree.sortColumn() - - return self.sortData(column) < other.sortData(column) - - def sortData(self, column): - return self._sortData.get(column, self.text(column)) - - def setSortData(self, column, data): - self._sortData[column] = data - - @property - def plugin(self): - return self.data(COLUMN_NAME, QtCore.Qt.ItemDataRole.UserRole) - - def enable(self, boolean, greyout=None): - if boolean is not None: - self.is_enabled = boolean - if self.is_enabled: - self.buttons['enable'].mode('enabled') - else: - self.buttons['enable'].mode('disabled') - if greyout is not None: - self.buttons['enable'].setEnabled(not greyout) - - -class PluginsOptionsPage(OptionsPage): - - NAME = 'plugins' - TITLE = N_("Plugins") - PARENT = None - SORT_ORDER = 70 - ACTIVE = True - HELP_URL = "/config/options_plugins.html" - - def __init__(self, parent=None): - super().__init__(parent=parent) - self.ui = Ui_PluginsOptionsPage() - self.ui.setupUi(self) - plugins = self.ui.plugins - - # fix for PICARD-1226, QT bug (https://bugreports.qt.io/browse/QTBUG-22572) workaround - plugins.setStyleSheet('') - - plugins.itemSelectionChanged.connect(self.change_details) - plugins.mimeTypes = self.mimeTypes - plugins.dropEvent = self.dropEvent - plugins.dragEnterEvent = self.dragEnterEvent - plugins.dragMoveEvent = self.dragMoveEvent - - self.ui.install_plugin.clicked.connect(self.open_plugins) - self.ui.folder_open.clicked.connect(self.open_plugin_dir) - self.ui.reload_list_of_plugins.clicked.connect(self.reload_list_of_plugins) - - self.manager = self.tagger.pluginmanager - self.manager.plugin_installed.connect(self.plugin_installed) - self.manager.plugin_updated.connect(self.plugin_updated) - self.manager.plugin_removed.connect(self.plugin_removed) - self.manager.plugin_errored.connect(self.plugin_loading_error) - - self._preserve = {} - self._preserve_selected = None - - self.icons = { - 'download': self.create_icon('plugin-download'), - 'update': self.create_icon('plugin-update'), - 'uninstall': self.create_icon('plugin-uninstall'), - 'enabled': self.create_icon('plugin-enabled'), - 'disabled': self.create_icon('plugin-disabled'), - } - - def create_icon(self, icon_name): - if theme.is_dark_theme: - icon_name += '-dark' - return icontheme.lookup(icon_name, icontheme.ICON_SIZE_MENU) - - def items(self): - iterator = QTreeWidgetItemIterator(self.ui.plugins, QTreeWidgetItemIterator.IteratorFlag.All) - while iterator.value(): - item = iterator.value() - iterator += 1 - yield item - - def find_item_by_plugin_name(self, plugin_name): - for item in self.items(): - if plugin_name == item.plugin.module_name: - return item - return None - - def selected_item(self): - try: - return self.ui.plugins.selectedItems()[COLUMN_NAME] - except IndexError: - return None - - def save_state(self): - header = self.ui.plugins.header() - config = get_config() - config.persist['plugins_list_state'] = header.saveState() - config.persist['plugins_list_sort_section'] = header.sortIndicatorSection() - config.persist['plugins_list_sort_order'] = header.sortIndicatorOrder() - - def set_current_item(self, item, scroll=False): - if scroll: - self.ui.plugins.scrollToItem(item) - self.ui.plugins.setCurrentItem(item) - self.refresh_details(item) - - def restore_state(self): - header = self.ui.plugins.header() - config = get_config() - header.restoreState(config.persist['plugins_list_state']) - idx = config.persist['plugins_list_sort_section'] - order = config.persist['plugins_list_sort_order'] - header.setSortIndicator(idx, order) - self.ui.plugins.sortByColumn(idx, order) - - @staticmethod - def is_plugin_enabled(plugin): - config = get_config() - return bool(plugin.module_name in config.setting['enabled_plugins']) - - def available_plugins_name_version(self): - return {p.module_name: p.version for p in self.manager.available_plugins} - - def installable_plugins(self): - if self.manager.available_plugins is not None: - installed_plugins = [plugin.module_name for plugin in - self.installed_plugins()] - for plugin in sorted(self.manager.available_plugins, - key=attrgetter('name')): - if plugin.module_name not in installed_plugins: - yield plugin - - def installed_plugins(self): - return sorted(self.manager.plugins, key=attrgetter('name')) - - def enabled_plugins(self): - return [item.plugin.module_name for item in self.items() if item.is_enabled] - - def _populate(self): - self._user_interaction(False) - if self.manager.available_plugins is None: - available_plugins = {} - self.manager.query_available_plugins(self._reload) - else: - available_plugins = self.available_plugins_name_version() - - self.ui.details.setText("") - - self.ui.plugins.setSortingEnabled(False) - for plugin in self.installed_plugins(): - new_version = None - if plugin.module_name in available_plugins: - latest = available_plugins[plugin.module_name] - if latest > plugin.version: - new_version = latest - self.update_plugin_item(None, plugin, - enabled=self.is_plugin_enabled(plugin), - new_version=new_version, - is_installed=True - ) - - for plugin in self.installable_plugins(): - self.update_plugin_item(None, plugin, enabled=False, - is_installed=False) - - self.ui.plugins.setSortingEnabled(True) - self._user_interaction(True) - header = self.ui.plugins.header() - header.setStretchLastSection(False) - header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.Fixed) - header.setSectionResizeMode(COLUMN_NAME, QtWidgets.QHeaderView.ResizeMode.Stretch) - header.setSectionResizeMode(COLUMN_VERSION, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) - header.setSectionResizeMode(COLUMN_ACTIONS, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) - - def _remove_all(self): - for item in self.items(): - idx = self.ui.plugins.indexOfTopLevelItem(item) - self.ui.plugins.takeTopLevelItem(idx) - - def restore_defaults(self): - self._user_interaction(False) - self._remove_all() - super().restore_defaults() - self.set_current_item(self.ui.plugins.topLevelItem(0), scroll=True) - - def load(self): - self._populate() - self.restore_state() - - def _preserve_plugins_states(self): - self._preserve = {item.plugin.module_name: item.save_state() for item in self.items()} - item = self.selected_item() - if item: - self._preserve_selected = item.plugin.module_name - else: - self._preserve_selected = None - - def _restore_plugins_states(self): - for item in self.items(): - plugin = item.plugin - if plugin.module_name in self._preserve: - item.restore_state(self._preserve[plugin.module_name]) - if self._preserve_selected == plugin.module_name: - self.set_current_item(item, scroll=True) - - def _reload(self): - if self.deleted: - return - self._remove_all() - self._populate() - self._restore_plugins_states() - - def _user_interaction(self, enabled): - self.ui.plugins.blockSignals(not enabled) - self.ui.plugins_container.setEnabled(enabled) - - def reload_list_of_plugins(self): - self.ui.details.setText(_("Reloading list of available plugins…")) - self._user_interaction(False) - self._preserve_plugins_states() - self.manager.query_available_plugins(callback=self._reload) - - def plugin_loading_error(self, plugin_name, error): - QtWidgets.QMessageBox.critical( - self, - _('Plugin "%(plugin)s"') % {'plugin': plugin_name}, - _('An error occurred while loading the plugin "%(plugin)s":\n\n%(error)s') % { - 'plugin': plugin_name, - 'error': error, - }) - - def plugin_installed(self, plugin): - log.debug("Plugin %r installed", plugin.name) - if not plugin.compatible: - params = {'plugin': plugin.name} - QtWidgets.QMessageBox.warning( - self, - _('Plugin "%(plugin)s"') % params, - _('The plugin "%(plugin)s" is not compatible with this version of Picard.') % params - ) - return - item = self.find_item_by_plugin_name(plugin.module_name) - if item: - self.update_plugin_item(item, plugin, make_current=True, - enabled=True, is_installed=True) - else: - self._reload() - item = self.find_item_by_plugin_name(plugin.module_name) - if item: - self.set_current_item(item, scroll=True) - - def plugin_updated(self, plugin_name): - log.debug("Plugin %r updated", plugin_name) - item = self.find_item_by_plugin_name(plugin_name) - if item: - plugin = item.plugin - QtWidgets.QMessageBox.information( - self, - _('Plugin "%(plugin)s"') % {'plugin': plugin_name}, - _('The plugin "%(plugin)s" will be upgraded to version %(version)s on next run of Picard.') % { - 'plugin': plugin.name, - 'version': item.new_version.short_str(), - }) - - item.upgrade_to_version = item.new_version - self.update_plugin_item(item, plugin, make_current=True) - - def plugin_removed(self, plugin_name): - log.debug("Plugin %r removed", plugin_name) - item = self.find_item_by_plugin_name(plugin_name) - if item: - if self.manager.is_available(plugin_name): - self.update_plugin_item(item, None, make_current=True, - is_installed=False) - else: # Remove local plugin - self.ui.plugins.invisibleRootItem().removeChild(item) - - def uninstall_plugin(self, item): - plugin = item.plugin - params = {'plugin': plugin.name} - buttonReply = QtWidgets.QMessageBox.question( - self, - _('Uninstall plugin "%(plugin)s"?') % params, - _('Do you really want to uninstall the plugin "%(plugin)s"?') % params, - QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, - QtWidgets.QMessageBox.StandardButton.No - ) - if buttonReply == QtWidgets.QMessageBox.StandardButton.Yes: - self.manager.remove_plugin(plugin.module_name, with_update=True) - - def update_plugin_item(self, item, plugin, - make_current=False, - enabled=None, - new_version=None, - is_installed=None - ): - if item is None: - item = PluginTreeWidgetItem(self.icons, self.ui.plugins) - if plugin is not None: - item.setData(COLUMN_NAME, QtCore.Qt.ItemDataRole.UserRole, plugin) - else: - plugin = item.plugin - if new_version is not None: - item.new_version = new_version - if is_installed is not None: - item.is_installed = is_installed - if enabled is None: - enabled = item.is_enabled - - def update_text(): - if item.new_version is not None: - version = "%s → %s" % (plugin.version.short_str(), - item.new_version.short_str()) - else: - version = plugin.version.short_str() - - if item.installed_font is None: - item.installed_font = item.font(COLUMN_NAME) - if item.enabled_font is None: - item.enabled_font = QtGui.QFont(item.installed_font) - item.enabled_font.setBold(True) - if item.available_font is None: - item.available_font = QtGui.QFont(item.installed_font) - - if item.is_enabled: - item.setFont(COLUMN_NAME, item.enabled_font) - else: - if item.is_installed: - item.setFont(COLUMN_NAME, item.installed_font) - else: - item.setFont(COLUMN_NAME, item.available_font) - - item.setText(COLUMN_NAME, plugin.name) - item.setText(COLUMN_VERSION, version) - - def toggle_enable(): - item.enable(not item.is_enabled, greyout=not item.is_installed) - log.debug("Plugin %r enabled: %r", item.plugin.name, item.is_enabled) - update_text() - - reconnect(item.buttons['enable'].clicked, toggle_enable) - - install_enabled = not item.is_installed or bool(item.new_version) - if item.upgrade_to_version: - if item.upgrade_to_version != item.new_version: - # case when a new version is known after a plugin was marked for update - install_enabled = True - else: - install_enabled = False - - if install_enabled: - if item.new_version is not None: - def download_and_update(): - self.download_plugin(item, update=True) - - reconnect(item.buttons['update'].clicked, download_and_update) - item.buttons['install'].mode('hide') - item.buttons['update'].mode('show') - else: - def download_and_install(): - self.download_plugin(item) - - reconnect(item.buttons['install'].clicked, download_and_install) - item.buttons['install'].mode('show') - item.buttons['update'].mode('hide') - - if item.is_installed: - item.buttons['install'].mode('hide') - item.buttons['uninstall'].mode( - 'show' if plugin.is_user_installed else 'hide') - item.enable(enabled, greyout=False) - - def uninstall_processor(): - self.uninstall_plugin(item) - - reconnect(item.buttons['uninstall'].clicked, uninstall_processor) - else: - item.buttons['uninstall'].mode('hide') - item.enable(False) - item.buttons['enable'].mode('hide') - - update_text() - - if make_current: - self.set_current_item(item) - - actions_sort_score = 2 - if item.is_installed: - if item.is_enabled: - actions_sort_score = 0 - else: - actions_sort_score = 1 - - item.setSortData(COLUMN_ACTIONS, actions_sort_score) - item.setSortData(COLUMN_NAME, plugin.name.lower()) - - def v2int(elem): - try: - return int(elem) - except ValueError: - return 0 - item.setSortData(COLUMN_VERSION, plugin.version) - - return item - - def save(self): - config = get_config() - config.setting['enabled_plugins'] = self.enabled_plugins() - self.save_state() - - def refresh_details(self, item): - plugin = item.plugin - text = [] - if item.new_version is not None: - if item.upgrade_to_version: - label = _("Restart Picard to upgrade to new version") - else: - label = _("New version available") - version_str = item.new_version.short_str() - text.append("{0}: {1}".format(label, version_str)) - if plugin.description: - text.append(plugin.description + "
") - infos = [ - (_("Name"), escape(plugin.name)), - (_("Authors"), self.link_authors(plugin.author)), - (_("License"), plugin.license), - (_("Files"), escape(plugin.files_list)), - (_("User Guide"), self.link_user_guide(plugin.user_guide_url)), - ] - for label, value in infos: - if value: - text.append("{0}: {1}".format(label, value)) - self.ui.details.setText("

{0}

".format("
\n".join(text))) - - @staticmethod - def link_authors(authors): - formatted_authors = [] - re_author = re.compile(r"(?P.*?)\s*<(?P.*?@.*?)>") - for author in authors.split(','): - author = author.strip() - match_ = re_author.fullmatch(author) - if match_: - author_str = '{author}'.format( - email=escape(match_['email']), - author=escape(match_['author']), - ) - formatted_authors.append(author_str) - else: - formatted_authors.append(escape(author)) - return ', '.join(formatted_authors) - - @staticmethod - def link_user_guide(user_guide): - if user_guide: - user_guide = '{url}'.format( - url=escape(user_guide) - ) - return user_guide - - def change_details(self): - item = self.selected_item() - if item: - self.refresh_details(item) - - def open_plugins(self): - files, _filter = FileDialog.getOpenFileNames( - parent=self, - dir=QtCore.QDir.homePath(), - filter="Picard plugin (*.py *.pyc *.zip)", - ) - if files: - for path in files: - self.manager.install_plugin(path) - - def download_plugin(self, item, update=False): - plugin = item.plugin - - self.tagger.webservice.get_url( - url=PLUGINS_API['urls']['download'], - handler=partial(self.download_handler, update, plugin=plugin), - parse_response_type=None, - priority=True, - important=True, - unencoded_queryargs={'id': plugin.module_name, 'version': plugin.version.short_str()}, - ) - - def download_handler(self, update, response, reply, error, plugin): - if self.deleted: - return - if error: - params = {'plugin': plugin.module_name} - msgbox = QtWidgets.QMessageBox(self) - msgbox.setText(_('The plugin "%(plugin)s" could not be downloaded.') % params) - msgbox.setInformativeText(_("Please try again later.")) - msgbox.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) - msgbox.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Ok) - msgbox.exec() - log.error('Error occurred while trying to download the plugin: "%(plugin)s"', params) - return - - self.manager.install_plugin( - None, - update=update, - plugin_name=plugin.module_name, - plugin_data=response, - ) - - @staticmethod - def open_plugin_dir(): - open_local_path(USER_PLUGIN_DIR) - - def mimeTypes(self): - return ['text/uri-list'] - - def dragEnterEvent(self, event): - event.setDropAction(QtCore.Qt.DropAction.CopyAction) - event.accept() - - def dragMoveEvent(self, event): - event.setDropAction(QtCore.Qt.DropAction.CopyAction) - event.accept() - - def dropEvent(self, event): - if event.proposedAction() == QtCore.Qt.DropAction.IgnoreAction: - event.acceptProposedAction() - return - - for path in (os.path.normpath(u.toLocalFile()) for u in event.mimeData().urls()): - self.manager.install_plugin(path) - - event.setDropAction(QtCore.Qt.DropAction.CopyAction) - event.accept() - - -register_options_page(PluginsOptionsPage)