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):