%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/preferences/ |
Current File : //lib/calibre/calibre/gui2/preferences/plugins.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __docformat__ = 'restructuredtext en' import textwrap, os from collections import OrderedDict from qt.core import (Qt, QMenu, QModelIndex, QAbstractItemModel, QIcon, QBrush, QDialog, QItemSelectionModel, QAbstractItemView) from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences.plugins_ui import Ui_Form from calibre.customize import PluginInstallationType from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin, disable_plugin, plugin_customization, add_plugin, remove_plugin, NameConflict) from calibre.gui2 import (error_dialog, info_dialog, choose_files, question_dialog, gprefs) from calibre.gui2.dialogs.confirm_delete import confirm from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.icu import lower from calibre.constants import iswindows from polyglot.builtins import iteritems, itervalues class AdaptSQP(SearchQueryParser): def __init__(self, *args, **kwargs): pass class PluginModel(QAbstractItemModel, AdaptSQP): # {{{ def __init__(self, show_only_user_plugins=False): QAbstractItemModel.__init__(self) SearchQueryParser.__init__(self, ['all']) self.show_only_user_plugins = show_only_user_plugins self.icon = QIcon(I('plugins.png')) p = QIcon(self.icon).pixmap(64, 64, QIcon.Mode.Disabled, QIcon.State.On) self.disabled_icon = QIcon(p) self._p = p self.populate() def toggle_shown_plugins(self, show_only_user_plugins): self.show_only_user_plugins = show_only_user_plugins self.beginResetModel() self.populate() self.endResetModel() def populate(self): self._data = {} for plugin in initialized_plugins(): if (getattr(plugin, 'installation_type', None) is not PluginInstallationType.EXTERNAL and self.show_only_user_plugins): continue if plugin.type not in self._data: self._data[plugin.type] = [plugin] else: self._data[plugin.type].append(plugin) self.categories = sorted(self._data.keys()) for plugins in self._data.values(): plugins.sort(key=lambda x: x.name.lower()) def universal_set(self): ans = set() for c, category in enumerate(self.categories): ans.add((c, -1)) for p, plugin in enumerate(self._data[category]): ans.add((c, p)) return ans def get_matches(self, location, query, candidates=None): if candidates is None: candidates = self.universal_set() ans = set() if not query: return ans query = lower(query) for c, p in candidates: if p < 0: if query in lower(self.categories[c]): ans.add((c, p)) continue else: try: plugin = self._data[self.categories[c]][p] except: continue if query in lower(plugin.name) or query in lower(plugin.author) or \ query in lower(plugin.description): ans.add((c, p)) return ans def find(self, query): query = query.strip() if not query: return QModelIndex() matches = self.parse(query) if not matches: return QModelIndex() matches = list(sorted(matches)) c, p = matches[0] cat_idx = self.index(c, 0, QModelIndex()) if p == -1: return cat_idx return self.index(p, 0, cat_idx) def find_next(self, idx, query, backwards=False): query = query.strip() if not query: return idx matches = self.parse(query) if not matches: return idx if idx.parent().isValid(): loc = (idx.parent().row(), idx.row()) else: loc = (idx.row(), -1) if loc not in matches: return self.find(query) if len(matches) == 1: return QModelIndex() matches = list(sorted(matches)) i = matches.index(loc) if backwards: ans = i - 1 if i - 1 >= 0 else len(matches)-1 else: ans = i + 1 if i + 1 < len(matches) else 0 ans = matches[ans] return self.index(ans[0], 0, QModelIndex()) if ans[1] < 0 else \ self.index(ans[1], 0, self.index(ans[0], 0, QModelIndex())) def index(self, row, column, parent=QModelIndex()): if not self.hasIndex(row, column, parent): return QModelIndex() if parent.isValid(): return self.createIndex(row, column, 1+parent.row()) else: return self.createIndex(row, column, 0) def parent(self, index): if not index.isValid() or index.internalId() == 0: return QModelIndex() return self.createIndex(index.internalId()-1, 0, 0) def rowCount(self, parent): if not parent.isValid(): return len(self.categories) if parent.internalId() == 0: category = self.categories[parent.row()] return len(self._data[category]) return 0 def columnCount(self, parent): return 1 def index_to_plugin(self, index): category = self.categories[index.parent().row()] return self._data[category][index.row()] def plugin_to_index(self, plugin): for i, category in enumerate(self.categories): parent = self.index(i, 0, QModelIndex()) for j, p in enumerate(self._data[category]): if plugin == p: return self.index(j, 0, parent) return QModelIndex() def plugin_to_index_by_properties(self, plugin): for i, category in enumerate(self.categories): parent = self.index(i, 0, QModelIndex()) for j, p in enumerate(self._data[category]): if plugin.name == p.name and plugin.type == p.type and \ plugin.author == p.author and plugin.version == p.version: return self.index(j, 0, parent) return QModelIndex() def refresh_plugin(self, plugin, rescan=False): if rescan: self.populate() idx = self.plugin_to_index(plugin) self.dataChanged.emit(idx, idx) def flags(self, index): if not index.isValid(): return 0 flags = Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled return flags def data(self, index, role): if not index.isValid(): return None if index.internalId() == 0: if role == Qt.ItemDataRole.DisplayRole: return self.categories[index.row()] else: plugin = self.index_to_plugin(index) disabled = is_disabled(plugin) if role == Qt.ItemDataRole.DisplayRole: ver = '.'.join(map(str, plugin.version)) desc = '\n'.join(textwrap.wrap(plugin.description, 100)) ans='%s (%s) %s %s\n%s'%(plugin.name, ver, _('by'), plugin.author, desc) c = plugin_customization(plugin) if c and not disabled: ans += _('\nCustomization: ')+c if disabled: ans += _('\n\nThis plugin has been disabled') if plugin.installation_type is PluginInstallationType.SYSTEM: ans += _('\n\nThis plugin is installed system-wide and can not be managed from within calibre') return (ans) if role == Qt.ItemDataRole.DecorationRole: return self.disabled_icon if disabled else self.icon if role == Qt.ItemDataRole.ForegroundRole and disabled: return (QBrush(Qt.GlobalColor.gray)) if role == Qt.ItemDataRole.UserRole: return plugin return None # }}} class ConfigWidget(ConfigWidgetBase, Ui_Form): supports_restoring_to_defaults = False def genesis(self, gui): self.gui = gui self._plugin_model = PluginModel(self.user_installed_plugins.isChecked()) self.plugin_view.setModel(self._plugin_model) self.plugin_view.setStyleSheet( "QTreeView::item { padding-bottom: 10px;}") self.plugin_view.doubleClicked.connect(self.double_clicked) self.toggle_plugin_button.clicked.connect(self.toggle_plugin) self.customize_plugin_button.clicked.connect(self.customize_plugin) self.remove_plugin_button.clicked.connect(self.remove_plugin) self.button_plugin_add.clicked.connect(self.add_plugin) self.button_plugin_updates.clicked.connect(self.update_plugins) self.button_plugin_new.clicked.connect(self.get_plugins) self.search.initialize('plugin_search_history', help_text=_('Search for plugin')) self.search.search.connect(self.find) self.next_button.clicked.connect(self.find_next) self.previous_button.clicked.connect(self.find_previous) self.changed_signal.connect(self.reload_store_plugins) self.user_installed_plugins.stateChanged.connect(self.show_user_installed_plugins) self.plugin_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.plugin_view.customContextMenuRequested.connect(self.show_context_menu) def show_context_menu(self, pos): menu = QMenu(self) menu.addAction(QIcon.ic('plus.png'), _('Expand all'), self.plugin_view.expandAll) menu.addAction(QIcon.ic('minus.png'), _('Collapse all'), self.plugin_view.collapseAll) menu.exec(self.plugin_view.mapToGlobal(pos)) def show_user_installed_plugins(self, state): self._plugin_model.toggle_shown_plugins(self.user_installed_plugins.isChecked()) def find(self, query): idx = self._plugin_model.find(query) if not idx.isValid(): return info_dialog(self, _('No matches'), _('Could not find any matching plugins'), show=True, show_copy_button=False) self.highlight_index(idx) def highlight_index(self, idx): self.plugin_view.selectionModel().select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect) self.plugin_view.setCurrentIndex(idx) self.plugin_view.setFocus(Qt.FocusReason.OtherFocusReason) self.plugin_view.scrollTo(idx, QAbstractItemView.ScrollHint.EnsureVisible) def find_next(self, *args): idx = self.plugin_view.currentIndex() if not idx.isValid(): idx = self._plugin_model.index(0, 0) idx = self._plugin_model.find_next(idx, str(self.search.currentText())) self.highlight_index(idx) def find_previous(self, *args): idx = self.plugin_view.currentIndex() if not idx.isValid(): idx = self._plugin_model.index(0, 0) idx = self._plugin_model.find_next(idx, str(self.search.currentText()), backwards=True) self.highlight_index(idx) def toggle_plugin(self, *args): self.modify_plugin(op='toggle') def double_clicked(self, index): if index.parent().isValid(): self.modify_plugin(op='customize') def customize_plugin(self, *args): self.modify_plugin(op='customize') def remove_plugin(self, *args): self.modify_plugin(op='remove') def add_plugin(self): info = '' if iswindows else ' [.zip %s]'%_('files') path = choose_files(self, 'add a plugin dialog', _('Add plugin'), filters=[(_('Plugins') + info, ['zip'])], all_files=False, select_only_single_file=True) if not path: return path = path[0] if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'): if not question_dialog(self, _('Are you sure?'), '<p>' + _('Installing plugins is a <b>security risk</b>. ' 'Plugins can contain a virus/malware. ' 'Only install it if you got it from a trusted source.' ' Are you sure you want to proceed?'), show_copy_button=False): return from calibre.customize.ui import config installed_plugins = frozenset(config['plugins']) try: plugin = add_plugin(path) except NameConflict as e: return error_dialog(self, _('Already exists'), str(e), show=True) self._plugin_model.beginResetModel() self._plugin_model.populate() self._plugin_model.endResetModel() self.changed_signal.emit() self.check_for_add_to_toolbars(plugin, previously_installed=plugin.name in installed_plugins) info_dialog(self, _('Success'), _('Plugin <b>{0}</b> successfully installed under <b>' '{1}</b>. You may have to restart calibre ' 'for the plugin to take effect.').format(plugin.name, plugin.type), show=True, show_copy_button=False) idx = self._plugin_model.plugin_to_index_by_properties(plugin) if idx.isValid(): self.highlight_index(idx) else: error_dialog(self, _('No valid plugin path'), _('%s is not a valid plugin path')%path).exec() def modify_plugin(self, op=''): index = self.plugin_view.currentIndex() if index.isValid(): if not index.parent().isValid(): name = str(index.data() or '') return error_dialog(self, _('Error'), '<p>'+ _('Select an actual plugin under <b>%s</b> to customize')%name, show=True, show_copy_button=False) plugin = self._plugin_model.index_to_plugin(index) if op == 'toggle': if not plugin.can_be_disabled: info_dialog(self, _('Plugin cannot be disabled'), _('Disabling the plugin %s is not allowed')%plugin.name, show=True, show_copy_button=False) return if is_disabled(plugin): enable_plugin(plugin) else: disable_plugin(plugin) self._plugin_model.refresh_plugin(plugin) self.changed_signal.emit() if op == 'customize': if not plugin.is_customizable(): info_dialog(self, _('Plugin not customizable'), _('Plugin: %s does not need customization')%plugin.name).exec() return self.changed_signal.emit() from calibre.customize import InterfaceActionBase if isinstance(plugin, InterfaceActionBase) and not getattr(plugin, 'actual_iaction_plugin_loaded', False): return error_dialog(self, _('Must restart'), _('You must restart calibre before you can' ' configure the <b>%s</b> plugin')%plugin.name, show=True) if plugin.do_user_config(self.gui): self._plugin_model.refresh_plugin(plugin) elif op == 'remove': if not confirm('<p>' + _('Are you sure you want to remove the plugin: %s?')% f'<b>{plugin.name}</b>', 'confirm_plugin_removal_msg', parent=self): return msg = _('Plugin <b>{0}</b> successfully removed. You will have' ' to restart calibre for it to be completely removed.').format(plugin.name) if remove_plugin(plugin): self._plugin_model.beginResetModel() self._plugin_model.populate() self._plugin_model.endResetModel() self.changed_signal.emit() info_dialog(self, _('Success'), msg, show=True, show_copy_button=False) else: error_dialog(self, _('Cannot remove builtin plugin'), plugin.name + _(' cannot be removed. It is a ' 'builtin plugin. Try disabling it instead.')).exec() def get_plugins(self): self.update_plugins(not_installed=True) def update_plugins(self, not_installed=False): from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, FILTER_UPDATE_AVAILABLE, FILTER_NOT_INSTALLED) mode = FILTER_NOT_INSTALLED if not_installed else FILTER_UPDATE_AVAILABLE d = PluginUpdaterDialog(self.gui, initial_filter=mode) d.exec() self._plugin_model.beginResetModel() self._plugin_model.populate() self._plugin_model.endResetModel() self.changed_signal.emit() if d.do_restart: self.restart_now.emit() def reload_store_plugins(self): self.gui.load_store_plugins() if 'Store' in self.gui.iactions: self.gui.iactions['Store'].load_menu() def check_for_add_to_toolbars(self, plugin, previously_installed=True): from calibre.gui2.preferences.toolbar import ConfigWidget from calibre.customize import InterfaceActionBase, EditBookToolPlugin if isinstance(plugin, EditBookToolPlugin): return self.check_for_add_to_editor_toolbar(plugin, previously_installed) if not isinstance(plugin, InterfaceActionBase): return all_locations = OrderedDict(ConfigWidget.LOCATIONS) try: plugin_action = plugin.load_actual_plugin(self.gui) except: # Broken plugin, fails to initialize. Given that, it's probably # already configured, so we can just quit. return installed_actions = OrderedDict([ (key, list(gprefs.get('action-layout-'+key, []))) for key in all_locations]) # If this is an update, do nothing if previously_installed: return # If already installed in a GUI container, do nothing for action_names in itervalues(installed_actions): if plugin_action.name in action_names: return allowed_locations = [(key, text) for key, text in iteritems(all_locations) if key not in plugin_action.dont_add_to] if not allowed_locations: return # This plugin doesn't want to live in the GUI from calibre.gui2.dialogs.choose_plugin_toolbars import ChoosePluginToolbarsDialog d = ChoosePluginToolbarsDialog(self, plugin_action, allowed_locations) if d.exec() == QDialog.DialogCode.Accepted: for key, text in d.selected_locations(): installed_actions = list(gprefs.get('action-layout-'+key, [])) installed_actions.append(plugin_action.name) gprefs['action-layout-'+key] = tuple(installed_actions) def check_for_add_to_editor_toolbar(self, plugin, previously_installed): if not previously_installed: from calibre.utils.config import JSONConfig prefs = JSONConfig('newly-installed-editor-plugins') pl = set(prefs.get('newly_installed_plugins', ())) pl.add(plugin.name) prefs['newly_installed_plugins'] = sorted(pl) if __name__ == '__main__': from qt.core import QApplication app = QApplication([]) test_widget('Advanced', 'Plugins')