mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
A demo plugin that shows how to run webengine from a user interface plugin
This commit is contained in:
parent
737bfa44cb
commit
e95ab50cba
@ -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`.
|
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 <https://calibre-ebook.com/downloads/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
|
Adding translations to your plugin
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
31
manual/plugin_examples/webengine_demo/__init__.py
Normal file
31
manual/plugin_examples/webengine_demo/__init__.py
Normal file
@ -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 <kovid at kovidgoyal.net>
|
||||||
|
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'
|
BIN
manual/plugin_examples/webengine_demo/images/icon.png
Normal file
BIN
manual/plugin_examples/webengine_demo/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
154
manual/plugin_examples/webengine_demo/main.py
Normal file
154
manual/plugin_examples/webengine_demo/main.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
#!/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__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
from PyQt5.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox, QLabel
|
||||||
|
|
||||||
|
from calibre_plugins.interface_demo.config import prefs
|
||||||
|
|
||||||
|
|
||||||
|
class DemoDialog(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, gui, icon, do_user_config):
|
||||||
|
QDialog.__init__(self, gui)
|
||||||
|
self.gui = gui
|
||||||
|
self.do_user_config = do_user_config
|
||||||
|
|
||||||
|
# The current database shown in the GUI
|
||||||
|
# db is an instance of the class LibraryDatabase from db/legacy.py
|
||||||
|
# This class has many, many methods that allow you to do a lot of
|
||||||
|
# things. For most purposes you should use db.new_api, which has
|
||||||
|
# a much nicer interface from db/cache.py
|
||||||
|
self.db = gui.current_db
|
||||||
|
|
||||||
|
self.l = QVBoxLayout()
|
||||||
|
self.setLayout(self.l)
|
||||||
|
|
||||||
|
self.label = QLabel(prefs['hello_world_msg'])
|
||||||
|
self.l.addWidget(self.label)
|
||||||
|
|
||||||
|
self.setWindowTitle('Interface Plugin Demo')
|
||||||
|
self.setWindowIcon(icon)
|
||||||
|
|
||||||
|
self.about_button = QPushButton('About', self)
|
||||||
|
self.about_button.clicked.connect(self.about)
|
||||||
|
self.l.addWidget(self.about_button)
|
||||||
|
|
||||||
|
self.marked_button = QPushButton(
|
||||||
|
'Show books with only one format in the calibre GUI', self)
|
||||||
|
self.marked_button.clicked.connect(self.marked)
|
||||||
|
self.l.addWidget(self.marked_button)
|
||||||
|
|
||||||
|
self.view_button = QPushButton(
|
||||||
|
'View the most recently added book', self)
|
||||||
|
self.view_button.clicked.connect(self.view)
|
||||||
|
self.l.addWidget(self.view_button)
|
||||||
|
|
||||||
|
self.update_metadata_button = QPushButton(
|
||||||
|
'Update metadata in a book\'s files', self)
|
||||||
|
self.update_metadata_button.clicked.connect(self.update_metadata)
|
||||||
|
self.l.addWidget(self.update_metadata_button)
|
||||||
|
|
||||||
|
self.conf_button = QPushButton(
|
||||||
|
'Configure this plugin', self)
|
||||||
|
self.conf_button.clicked.connect(self.config)
|
||||||
|
self.l.addWidget(self.conf_button)
|
||||||
|
|
||||||
|
self.resize(self.sizeHint())
|
||||||
|
|
||||||
|
def about(self):
|
||||||
|
# Get the about text from a file inside the plugin zip file
|
||||||
|
# The get_resources function is a builtin function defined for all your
|
||||||
|
# plugin code. It loads files from the plugin zip file. It returns
|
||||||
|
# the bytes from the specified file.
|
||||||
|
#
|
||||||
|
# Note that if you are loading more than one file, for performance, you
|
||||||
|
# should pass a list of names to get_resources. In this case,
|
||||||
|
# get_resources will return a dictionary mapping names to bytes. Names that
|
||||||
|
# are not found in the zip file will not be in the returned dictionary.
|
||||||
|
text = get_resources('about.txt')
|
||||||
|
QMessageBox.about(self, 'About the Interface Plugin Demo',
|
||||||
|
text.decode('utf-8'))
|
||||||
|
|
||||||
|
def marked(self):
|
||||||
|
''' Show books with only one format '''
|
||||||
|
db = self.db.new_api
|
||||||
|
matched_ids = {book_id for book_id in db.all_book_ids() if len(db.formats(book_id)) == 1}
|
||||||
|
# Mark the records with the matching ids
|
||||||
|
# new_api does not know anything about marked books, so we use the full
|
||||||
|
# db object
|
||||||
|
self.db.set_marked_ids(matched_ids)
|
||||||
|
|
||||||
|
# Tell the GUI to search for all marked records
|
||||||
|
self.gui.search.setEditText('marked:true')
|
||||||
|
self.gui.search.do_search()
|
||||||
|
|
||||||
|
def view(self):
|
||||||
|
''' View the most recently added book '''
|
||||||
|
most_recent = most_recent_id = None
|
||||||
|
db = self.db.new_api
|
||||||
|
for book_id, timestamp in db.all_field_for('timestamp', db.all_book_ids()).items():
|
||||||
|
if most_recent is None or timestamp > 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'])
|
72
manual/plugin_examples/webengine_demo/ui.py
Normal file
72
manual/plugin_examples/webengine_demo/ui.py
Normal file
@ -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 <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
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_()
|
@ -88,6 +88,16 @@ def store_dialog(args=sys.argv):
|
|||||||
main(args)
|
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):
|
def toc_dialog(**kw):
|
||||||
detach_gui()
|
detach_gui()
|
||||||
init_dbus()
|
init_dbus()
|
||||||
|
@ -35,6 +35,9 @@ PARALLEL_FUNCS = {
|
|||||||
'toc-dialog' :
|
'toc-dialog' :
|
||||||
('calibre.gui_launch', 'toc_dialog', None),
|
('calibre.gui_launch', 'toc_dialog', None),
|
||||||
|
|
||||||
|
'webengine-dialog' :
|
||||||
|
('calibre.gui_launch', 'webengine_dialog', None),
|
||||||
|
|
||||||
'render_pages' :
|
'render_pages' :
|
||||||
('calibre.ebooks.comic.input', 'render_pages', 'notification'),
|
('calibre.ebooks.comic.input', 'render_pages', 'notification'),
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user