From e95ab50cba4444eb1b5ed08ab533aec20dc52a41 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Sep 2019 20:55:06 +0530 Subject: [PATCH] A demo plugin that shows how to run webengine from a user interface plugin --- manual/creating_plugins.rst | 24 +++ .../webengine_demo/__init__.py | 31 ++++ .../webengine_demo/images/icon.png | Bin 0 -> 4248 bytes manual/plugin_examples/webengine_demo/main.py | 154 ++++++++++++++++++ .../plugin-import-name-webengine_demo.txt | 0 manual/plugin_examples/webengine_demo/ui.py | 72 ++++++++ src/calibre/gui_launch.py | 10 ++ src/calibre/utils/ipc/worker.py | 3 + 8 files changed, 294 insertions(+) create mode 100644 manual/plugin_examples/webengine_demo/__init__.py create mode 100644 manual/plugin_examples/webengine_demo/images/icon.png create mode 100644 manual/plugin_examples/webengine_demo/main.py create mode 100644 manual/plugin_examples/webengine_demo/plugin-import-name-webengine_demo.txt create mode 100644 manual/plugin_examples/webengine_demo/ui.py diff --git a/manual/creating_plugins.rst b/manual/creating_plugins.rst index ca8fc285d4..4be39495d1 100644 --- a/manual/creating_plugins.rst +++ b/manual/creating_plugins.rst @@ -248,6 +248,30 @@ The container object and various useful utility functions that can be reused in your plugin code are documented in :ref:`polish_api`. +Running User Interface plugins in a separate process +----------------------------------------------------------- + +If you are writing a user interface plugin that needs to make use +of Qt WebEngine, it cannot be run in the main calibre process as it +is not possible to use WebEngine there. Instead you can copy the data +your plugin needs to a temporary directory and run the plugin with that +data in a separate process. A simple example plugin follows that shows how +to do this. + +You can download the plugin from +`webengine_demo_plugin.zip `_. + +The important part of the plugin is in two functions: + +.. literalinclude:: plugin_examples/webengine_demo/ui.py + :lines: 47- + + +The ``show_demo()`` function asks the user for a URL and then runs +the ``main()`` function passing it that URL. The ``main()`` function +displays the URL in a ``QWebEngineView``. + + Adding translations to your plugin -------------------------------------- diff --git a/manual/plugin_examples/webengine_demo/__init__.py b/manual/plugin_examples/webengine_demo/__init__.py new file mode 100644 index 0000000000..9f4fc31476 --- /dev/null +++ b/manual/plugin_examples/webengine_demo/__init__.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python2 +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +# License: GPLv3 Copyright: 2019, Kovid Goyal +from __future__ import absolute_import, division, print_function, unicode_literals + + +# The class that all Interface Action plugin wrappers must inherit from +from calibre.customize import InterfaceActionBase + + +class WebEginePluginDemo(InterfaceActionBase): + ''' + This class is a simple wrapper that provides information about the actual + plugin class. The actual interface plugin class is called InterfacePlugin + and is defined in the ui.py file, as specified in the actual_plugin field + below. + + The reason for having two classes is that it allows the command line + calibre utilities to run without needing to load the GUI libraries. + ''' + name = 'WebEngine Plugin Demo' + description = 'A WebEngine plugin demo' + supported_platforms = ['windows', 'osx', 'linux'] + author = 'Kovid Goyal' + version = (1, 0, 0) + minimum_calibre_version = (3, 99, 3) + + #: This field defines the GUI plugin class that contains all the code + #: that actually does something. Its format is module_path:class_name + #: The specified class must be defined in the specified module. + actual_plugin = 'calibre_plugins.webengine_demo.ui:InterfacePlugin' diff --git a/manual/plugin_examples/webengine_demo/images/icon.png b/manual/plugin_examples/webengine_demo/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7512b6ef07f7b58594deec4906ac81d63ef742c3 GIT binary patch literal 4248 zcmV;J5NGd+P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 most_recent: + most_recent = timestamp + most_recent_id = book_id + + if most_recent_id is not None: + # Get a reference to the View plugin + view_plugin = self.gui.iactions['View'] + # Ask the view plugin to launch the viewer for row_number + view_plugin._view_calibre_books([most_recent_id]) + + def update_metadata(self): + ''' + Set the metadata in the files in the selected book's record to + match the current metadata in the database. + ''' + from calibre.ebooks.metadata.meta import set_metadata + from calibre.gui2 import error_dialog, info_dialog + + # Get currently selected books + rows = self.gui.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + return error_dialog(self.gui, 'Cannot update metadata', + 'No books selected', show=True) + # Map the rows to book ids + ids = list(map(self.gui.library_view.model().id, rows)) + db = self.db.new_api + for book_id in ids: + # Get the current metadata for this book from the db + mi = db.get_metadata(book_id, get_cover=True, cover_as_data=True) + fmts = db.formats(book_id) + if not fmts: + continue + for fmt in fmts: + fmt = fmt.lower() + # Get a python file object for the format. This will be either + # an in memory file or a temporary on disk file + ffile = db.format(book_id, fmt, as_file=True) + ffile.seek(0) + # Set metadata in the format + set_metadata(ffile, mi, fmt) + ffile.seek(0) + # Now replace the file in the calibre library with the updated + # file. We dont use add_format_with_hooks as the hooks were + # already run when the file was first added to calibre. + db.add_format(book_id, fmt, ffile, run_hooks=False) + + info_dialog(self, 'Updated files', + 'Updated the metadata in the files of %d book(s)'%len(ids), + show=True) + + def config(self): + self.do_user_config(parent=self) + # Apply the changes + self.label.setText(prefs['hello_world_msg']) diff --git a/manual/plugin_examples/webengine_demo/plugin-import-name-webengine_demo.txt b/manual/plugin_examples/webengine_demo/plugin-import-name-webengine_demo.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manual/plugin_examples/webengine_demo/ui.py b/manual/plugin_examples/webengine_demo/ui.py new file mode 100644 index 0000000000..41e105c936 --- /dev/null +++ b/manual/plugin_examples/webengine_demo/ui.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python2 +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import absolute_import, division, print_function, unicode_literals +# License: GPLv3 Copyright: 2019, Kovid Goyal + +if False: + # This is here to keep my python error checker from complaining about + # the builtin functions that will be defined by the plugin loading system + # You do not need this code in your plugins + get_icons = get_resources = None + +# The class that all interface action plugins must inherit from +from calibre.gui2.actions import InterfaceAction +from PyQt5.Qt import QInputDialog, QUrl + + +class InterfacePlugin(InterfaceAction): + + name = 'WebEngine Plugin Demo' + + # Declare the main action associated with this plugin + # The keyboard shortcut can be None if you dont want to use a keyboard + # shortcut. Remember that currently calibre has no central management for + # keyboard shortcuts, so try to use an unusual/unused shortcut. + action_spec = ('WebEngine Plugin Demo', None, + 'Run the WebEngine Plugin Demo', 'Ctrl+Shift+F2') + + def genesis(self): + # This method is called once per plugin, do initial setup here + + # Set the icon for this interface action + # The get_icons function is a builtin function defined for all your + # plugin code. It loads icons from the plugin zip file. It returns + # QIcon objects, if you want the actual data, use the analogous + # get_resources builtin function. + # + # Note that if you are loading more than one icon, for performance, you + # should pass a list of names to get_icons. In this case, get_icons + # will return a dictionary mapping names to QIcons. Names that + # are not found in the zip file will result in null QIcons. + icon = get_icons('images/icon.png') + + # The qaction is automatically created from the action_spec defined + # above + self.qaction.setIcon(icon) + self.qaction.triggered.connect(self.show_dialog) + + def show_dialog(self): + # Ask the user for a URL + url, ok = QInputDialog.getText(self.gui, 'Enter a URL', 'Enter a URL to browse below', text='https://calibre-ebook.com') + if not ok or not url: + return + # Launch a separate process to view the URL in WebEngine + self.gui.job_manager.launch_gui_app('webengine-dialog', kwargs={ + 'module':'calibre_plugins.webengine_demo.ui', 'url':url}) + + +def main(url): + # This function is run in a separate process and can do anything it likes, + # including use QWebEngine. Here it simply opens the passed in URL + # in a QWebEngineView + + # This import must happen before creating the Application() object + from PyQt5.QtWebEngineWidgets import QWebEngineView + + from calibre.gui2 import Application + app = Application([]) + w = QWebEngineView() + w.setUrl(QUrl(url)) + w.show() + w.raise_() + app.exec_() diff --git a/src/calibre/gui_launch.py b/src/calibre/gui_launch.py index 3d8f98303d..093a1f7b7f 100644 --- a/src/calibre/gui_launch.py +++ b/src/calibre/gui_launch.py @@ -88,6 +88,16 @@ def store_dialog(args=sys.argv): main(args) +def webengine_dialog(**kw): + detach_gui() + init_dbus() + from calibre.debug import load_user_plugins + load_user_plugins() + import importlib + m = importlib.import_module(kw.pop('module')) + getattr(m, kw.pop('entry_func', 'main'))(**kw) + + def toc_dialog(**kw): detach_gui() init_dbus() diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index f55ac3c951..42e4275beb 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -35,6 +35,9 @@ PARALLEL_FUNCS = { 'toc-dialog' : ('calibre.gui_launch', 'toc_dialog', None), + 'webengine-dialog' : + ('calibre.gui_launch', 'webengine_dialog', None), + 'render_pages' : ('calibre.ebooks.comic.input', 'render_pages', 'notification'),