diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index e2b8582af7..38f61d49ff 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -687,6 +687,8 @@ class StoreBase(Plugin): # {{{ class ViewerPlugin(Plugin): # {{{ + type = _('Viewer') + ''' These plugins are used to add functionality to the calibre viewer. ''' @@ -744,7 +746,8 @@ class ViewerPlugin(Plugin): # {{{ class EditBookToolPlugin(Plugin): # {{{ - minimum_calibre_version = (1, 45, 0) + type = _('Edit Book Tool') + minimum_calibre_version = (1, 46, 0) # }}} diff --git a/src/calibre/gui2/dialogs/plugin_updater.py b/src/calibre/gui2/dialogs/plugin_updater.py index 37bff80ffb..dc74efe9bb 100644 --- a/src/calibre/gui2/dialogs/plugin_updater.py +++ b/src/calibre/gui2/dialogs/plugin_updater.py @@ -707,6 +707,8 @@ class PluginUpdaterDialog(SizePersistedDialog): do_restart = False try: + from calibre.customize.ui import config + installed_plugins = frozenset(config['plugins']) try: plugin = add_plugin(zip_path) except NameConflict as e: @@ -715,7 +717,7 @@ class PluginUpdaterDialog(SizePersistedDialog): # Check for any toolbars to add to. widget = ConfigWidget(self.gui) widget.gui = self.gui - widget.check_for_add_to_toolbars(plugin) + widget.check_for_add_to_toolbars(plugin, previously_installed=plugin.name in installed_plugins) self.gui.status_bar.showMessage(_('Plugin installed: %s') % display_plugin.name) d = info_dialog(self.gui, _('Success'), _('Plugin {0} successfully installed under ' diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py index 9329477710..53e29daa00 100644 --- a/src/calibre/gui2/preferences/plugins.py +++ b/src/calibre/gui2/preferences/plugins.py @@ -304,6 +304,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): ' 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: @@ -312,7 +314,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self._plugin_model.populate() self._plugin_model.reset() self.changed_signal.emit() - self.check_for_add_to_toolbars(plugin) + self.check_for_add_to_toolbars(plugin, previously_installed=plugin.name in installed_plugins) info_dialog(self, _('Success'), _('Plugin {0} successfully installed under ' ' {1} plugins. You may have to restart calibre ' @@ -399,9 +401,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): if 'Store' in self.gui.iactions: self.gui.iactions['Store'].load_menu() - def check_for_add_to_toolbars(self, plugin): + def check_for_add_to_toolbars(self, plugin, previously_installed=True): from calibre.gui2.preferences.toolbar import ConfigWidget - from calibre.customize import InterfaceActionBase + 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 @@ -436,6 +441,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): 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.gui2.tweak_book.plugin import install_plugin + install_plugin(plugin) if __name__ == '__main__': from PyQt4.Qt import QApplication diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index 069c70cea5..9c59abac68 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -52,6 +52,7 @@ d['remove_unused_classes'] = False d['global_book_toolbar'] = [ 'new-file', 'open-book', 'save-book', None, 'global-undo', 'global-redo', 'create-checkpoint', None, 'donate', 'user-manual'] d['global_tools_toolbar'] = ['check-book', 'spell-check-book', 'edit-toc', 'insert-character', 'manage-fonts', 'smarten-punctuation', 'remove-unused-css'] +d['global_plugins_toolbar'] = [] d['editor_css_toolbar'] = ['pretty-current', 'insert-image'] d['editor_xml_toolbar'] = ['pretty-current', 'insert-tag'] d['editor_html_toolbar'] = ['fix-html-current', 'pretty-current', 'insert-image', 'insert-hyperlink', 'insert-tag', 'change-paragraph'] diff --git a/src/calibre/gui2/tweak_book/plugin.py b/src/calibre/gui2/tweak_book/plugin.py index 35a54143d5..95cbf0c328 100644 --- a/src/calibre/gui2/tweak_book/plugin.py +++ b/src/calibre/gui2/tweak_book/plugin.py @@ -6,6 +6,13 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' +import importlib + +from PyQt4.Qt import QToolButton + +from calibre import prints +from calibre.customize.ui import all_edit_book_tool_plugins +from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book.boss import get_boss class Tool(object): @@ -19,6 +26,9 @@ class Tool(object): #: If True the user can choose to place this tool in the plugins menu allowed_in_menu = True + #: The popup mode for the menu (if any) of the toolbar button. Possible values are 'delayed', 'instant', 'button' + toolbar_button_popup_mode = 'delayed' + @property def boss(self): ' The :class:`calibre.gui2.tweak_book.boss.Boss` object. Used to control the user interface. ' @@ -29,13 +39,105 @@ class Tool(object): ' The main window of the user interface ' return self.boss.gui + def register_shortcut(self, qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data): + ''' + Register a keyboard shortcut that will trigger the specified ``qaction``. This keyboard shortcut + will become automatically customizable by the user in the Keyboard section of the editor preferences. + + :param qaction: A QAction object, it will be triggered when the + configured key combination is pressed by the user. + :param unique_name: A unique name for this shortcut/action. It will be + used internally, it must not be shared by any other actions in this + plugin. + :param default_keys: A list of the default keyboard shortcuts. If not + specified no default shortcuts will be set. If the shortcuts specified + here conflict with either builtin shortcuts or shortcuts from user + configuration/other plugins, they will be ignored. In that case, users + will have to configure the shortcuts manually via Preferences. For example: + ``default_keys=('Ctrl+J', 'F9')``. + :param short_text: An optional short description of this action. If not + specified the text from the QAction will be used. + :param description: An optional longer description of this action, it + will be used in the preferences entry for this shortcut. + ''' + short_text = short_text or unicode(qaction.text()).replace('&&', '\0').replace('&', '').replace('\0', '&') + self.gui.keyboard.register_shortcut( + self.name + '_' + unique_name, short_text, default_keys=default_keys, + description=description or '', group=_('Plugins')) + def create_action(self, for_toolbar=True): ''' Create a QAction that will be added to either the plugins toolbar or the plugins menu depending on ``for_toolbar``. For example:: - def create_action(self, for_toolbar): - ac = QAction( + def create_action(self, for_toolbar=True): + ac = QAction(get_icons('myicon.png'), 'Do something') + if for_toolbar: + # We want the toolbar button to have a popup menu + menu = QMenu() + ac.setMenu(menu) + menu.addAction('Do something else') + subaction = menu.addAction('And another') + + # Register a keyboard shortcut for this toolbar action be + # careful to do this for only one of the toolbar action or + # the menu action, not both. + self.register_shortcut(ac, 'some-unique-name', default_keys=('Ctrl+K',)) + return ac + + .. seealso:: Method :meth:`register_shortcut`. ''' raise NotImplementedError() +def load_plugin_tools(plugin): + try: + main = importlib.import_module(plugin.__class__.__module__+'.main') + except ImportError: + import traceback + traceback.print_stack() + else: + for x in vars(main).itervalues(): + if isinstance(x, type) and x is not Tool and issubclass(x, Tool): + yield x() + +def plugin_action_sid(plugin, tool, for_toolbar=True): + return plugin.name + tool.name + ('toolbar' if for_toolbar else 'menu') + +def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_actions=None, plugin_menu_actions=None): + try: + ac = tool.create_action(for_toolbar=for_toolbar) + except Exception: + import traceback + traceback.print_stack() + return + sid = plugin_action_sid(plugin, tool, for_toolbar) + if actions is not None and sid in actions: + prints('The %s tool from the %s plugin has a non unique name, ignoring' % (tool.name, plugin.name)) + else: + if actions is not None: + actions[sid] = ac + ac.sid = sid + if for_toolbar: + if toolbar_actions is not None: + toolbar_actions[sid] = ac + ac.popup_mode = {'instant':QToolButton.InstantPopup, 'button':QToolButton.MenuButtonPopup}.get( + tool.toolbar_button_popup_mode, QToolButton.DelayedPopup) + else: + if plugin_menu_actions is not None: + plugin_menu_actions.append(ac) + return ac + +def create_plugin_actions(actions, toolbar_actions, plugin_menu_actions): + for plugin in all_edit_book_tool_plugins(): + for tool in load_plugin_tools(plugin): + if tool.allowed_in_toolbar: + create_plugin_action(plugin, tool, True, actions, toolbar_actions, plugin_menu_actions) + if tool.allowed_in_menu: + create_plugin_action(plugin, tool, False, actions, toolbar_actions, plugin_menu_actions) + +def install_plugin(plugin): + for tool in load_plugin_tools(plugin): + if tool.allowed_in_toolbar: + sid = plugin_action_sid(plugin, tool, True) + if sid not in tprefs['global_plugins_toolbar']: + tprefs['global_plugins_toolbar'] = tprefs['global_plugins_toolbar'] + [sid] diff --git a/src/calibre/gui2/tweak_book/preferences.py b/src/calibre/gui2/tweak_book/preferences.py index e043fc989d..bd16429a0f 100644 --- a/src/calibre/gui2/tweak_book/preferences.py +++ b/src/calibre/gui2/tweak_book/preferences.py @@ -339,6 +339,7 @@ class ToolbarSettings(QWidget): for name, text in ( ('global_book_toolbar', _('Book wide actions'),), ('global_tools_toolbar', _('Book wide tools'),), + ('global_plugins_toolbar', _('Book wide tools from third party plugins'),), ('editor_html_toolbar', ft % 'HTML',), ('editor_css_toolbar', ft % 'CSS',), ('editor_xml_toolbar', ft % 'XML',), diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 232d28f99c..831ec5a845 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -28,6 +28,7 @@ from calibre.gui2.tweak_book.job import BlockingJob from calibre.gui2.tweak_book.boss import Boss from calibre.gui2.tweak_book.undo import CheckpointView from calibre.gui2.tweak_book.preview import Preview +from calibre.gui2.tweak_book.plugin import create_plugin_actions from calibre.gui2.tweak_book.search import SearchPanel from calibre.gui2.tweak_book.check import Check from calibre.gui2.tweak_book.spell import SpellCheck @@ -38,7 +39,7 @@ from calibre.gui2.tweak_book.live_css import LiveCSS from calibre.gui2.tweak_book.manage_fonts import ManageFonts from calibre.gui2.tweak_book.editor.widget import register_text_editor_actions from calibre.gui2.tweak_book.editor.insert_resource import InsertImage -from calibre.utils.icu import character_name +from calibre.utils.icu import character_name, sort_key def open_donate(): open_url(QUrl('http://calibre-ebook.com/donate')) @@ -447,6 +448,10 @@ class Main(MainWindow): self.action_compare_book = treg('diff.png', _('&Compare to another book'), self.boss.compare_book, 'compare-book', (), _( 'Compare to another book')) + self.plugin_menu_actions = [] + + create_plugin_actions(actions, toolbar_actions, self.plugin_menu_actions) + def create_menubar(self): p, q = self.create_application_menubar() q.triggered.connect(self.action_quit.trigger) @@ -536,6 +541,11 @@ class Main(MainWindow): e.addSeparator() a(self.action_saved_searches) + if self.plugin_menu_actions: + e = b.addMenu(_('&Plugins')) + for ac in sorted(self.plugin_menu_actions, key=lambda x:sort_key(unicode(x.text()))): + e.addAction(ac) + e = b.addMenu(_('&Help')) a = e.addAction a(self.action_help) @@ -558,10 +568,11 @@ class Main(MainWindow): return b self.global_bar = create(_('Book tool bar'), 'global') self.tools_bar = create(_('Tools tool bar'), 'tools') + self.plugins_bar = create(_('Plugins tool bar'), 'plugins') self.populate_toolbars(animate=True) def populate_toolbars(self, animate=False): - self.global_bar.clear(), self.tools_bar.clear() + self.global_bar.clear(), self.tools_bar.clear(), self.plugins_bar.clear() def add(bar, ac): if ac is None: bar.addSeparator() @@ -591,6 +602,10 @@ class Main(MainWindow): for x in tprefs['global_tools_toolbar']: add(self.tools_bar, x) + for x in tprefs['global_plugins_toolbar']: + add(self.plugins_bar, x) + self.plugins_bar.setVisible(bool(tprefs['global_plugins_toolbar'])) + def create_docks(self): def create(name, oname):