mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Complete the example plugin and plugin creating tutorial
This commit is contained in:
parent
288905ee34
commit
03cc260fe5
@ -348,6 +348,9 @@ class UploadUserManual(Command): # {{{
|
||||
with ZipFile(f, 'w') as zf:
|
||||
for x in os.listdir('.'):
|
||||
zf.write(x)
|
||||
if os.path.isdir(x):
|
||||
for y in os.listdir(x):
|
||||
zf.write(os.path.join(x, y))
|
||||
bname = self.b(path) + '_plugin.zip'
|
||||
subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS,
|
||||
bname)])
|
||||
|
@ -40,7 +40,8 @@ def get_resources(zfp, name_or_list_of_names):
|
||||
try:
|
||||
ans[name] = zf.read(name)
|
||||
except:
|
||||
pass
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if len(names) == 1:
|
||||
ans = ans.pop(names[0], None)
|
||||
|
||||
@ -71,7 +72,7 @@ def get_icons(zfp, name_or_list_of_names):
|
||||
ians = {}
|
||||
for name in names:
|
||||
p = QPixmap()
|
||||
raw = ans.get('name', None)
|
||||
raw = ans.get(name, None)
|
||||
if raw:
|
||||
p.loadFromData(raw)
|
||||
ians[name] = QIcon(p)
|
||||
@ -136,8 +137,13 @@ class PluginLoader(object):
|
||||
raise ImportError('Plugin %r has no module named %r' %
|
||||
(plugin_name, import_name))
|
||||
with zipfile.ZipFile(zfp) as zf:
|
||||
try:
|
||||
code = zf.read(zinfo)
|
||||
compiled = compile(code, 'import_name', 'exec', dont_inherit=True)
|
||||
except:
|
||||
# Maybe the zip file changed from under us
|
||||
code = zf.read(zinfo.filename)
|
||||
compiled = compile(code, 'calibre_plugins.%s.%s'%(plugin_name,
|
||||
import_name), 'exec', dont_inherit=True)
|
||||
mod.__dict__['get_resources'] = partial(get_resources, zfp)
|
||||
mod.__dict__['get_icons'] = partial(get_icons, zfp)
|
||||
exec compiled in mod.__dict__
|
||||
@ -265,6 +271,9 @@ if __name__ == '__main__':
|
||||
if x[0] != '.':
|
||||
print ('Adding', x)
|
||||
zf.write(x)
|
||||
if os.path.isdir(x):
|
||||
for y in os.listdir(x):
|
||||
zf.write(os.path.join(x, y))
|
||||
add_plugin(f.name)
|
||||
print ('Added plugin from', sys.argv[-1])
|
||||
|
||||
|
@ -48,13 +48,111 @@ a more complex example that actually adds a component to the user interface.
|
||||
A User Interface plugin
|
||||
-------------------------
|
||||
|
||||
This plugin will be spread over a couple of files (to keep the code clean). It will show you how to get resources
|
||||
This plugin will be spread over a few files (to keep the code clean). It will show you how to get resources
|
||||
(images or data files) from the plugin zip file, how to create elements in the |app| user interface and how to access
|
||||
and query the books database in |app|.
|
||||
|
||||
You can download this plugin from `interface_demo_plugin.zip <http://calibre-ebook.com/downloads/interface_demo_plugin.zip>`_
|
||||
|
||||
The first thing to note is that this zip file has a lot more files in it, explained below, pay particular attention to
|
||||
``plugin-import-name-interface_demo.txt``.
|
||||
|
||||
**plugin-import-name-interface_demo.txt**
|
||||
An empty text file used to enable the multi-file plugin magic. This file must be present in all plugins that use
|
||||
more than one .py file. It should be empty and its filename must be of the form: plugin-import-name-**some_name**.txt
|
||||
The presence of this file allows you to import code from the .py files present inside the zip file, using a statement like::
|
||||
|
||||
from calibre_plugins.some_name.some_module import some_object
|
||||
|
||||
The prefix ``calibre_plugins`` must always be present. ``some_name`` comes from the filename of the empty text file.
|
||||
``some_module`` refers to :file:`some_module.py` file inside the zip file. Note that this importing is just as
|
||||
powerful as regular python imports. You can create packages and subpackages of .py modules inside the zip file,
|
||||
just like you would normally (by defining __init__.py in each sub directory), and everything should Just Work.
|
||||
|
||||
The name you use for ``some_name`` enters a global namespace shared by all plugins, **so make it as unique as possible**.
|
||||
But remember that it must be a valid python identifier (only alphabets, numbers and the underscore).
|
||||
|
||||
**__init__.py**
|
||||
As before, the file that defines the plugin class
|
||||
|
||||
**main.py**
|
||||
This file contains the actual code that does something useful
|
||||
|
||||
**ui.py**
|
||||
This file defines the interface part of the plugin
|
||||
|
||||
**images/icon.png**
|
||||
The icon for this plugin
|
||||
|
||||
**about.txt**
|
||||
A text file with information about the plugin
|
||||
|
||||
Now let's look at the code.
|
||||
|
||||
__init__.py
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
First, the obligatory ``__init__.py`` to define the plugin metadata:
|
||||
|
||||
.. literalinclude:: plugin_examples/interface_demo/__init__.py
|
||||
:lines: 10-
|
||||
|
||||
The only noteworthy feature is the field :attr:`actual_plugin`. Since |app| has both command line and GUI interfaces,
|
||||
GUI plugins like this one should not load any GUI libraries in __init__.py. The actual_plugin field does this for you,
|
||||
by telling |app| that the actual plugin is to be found in another file inside your zip archive, which will only be loaded
|
||||
in a GUI context.
|
||||
|
||||
Remember that for this to work, you must have a plugin-import-name-some_name.txt file in your plugin zip file,
|
||||
as discussed above.
|
||||
|
||||
|
||||
ui.py
|
||||
^^^^^^^^
|
||||
|
||||
Now let's look at ui.py which defines the actual GUI plugin. The source code is heavily commented and should be self explanatory:
|
||||
|
||||
.. literalinclude:: plugin_examples/interface_demo/ui.py
|
||||
:lines: 10-
|
||||
|
||||
main.py
|
||||
^^^^^^^^^
|
||||
|
||||
The actual logic to implement the Interface Plugin Demo dialog.
|
||||
|
||||
.. literalinclude:: plugin_examples/interface_demo/main.py
|
||||
:lines: 10-
|
||||
|
||||
Getting resources from the plugin zip file
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|app|'s plugin loading system defines a couple of builtin functions that allow you to conveniently get files from the plugin zip file.
|
||||
|
||||
**get_resources(name_or_list_of_names)**
|
||||
This function should be called with a list of paths to files inside the zip file. For example to access the file icon.png in
|
||||
the directory images in the zip file, you would use: ``images/icon.png``. Always use a forward slash as the path separator,
|
||||
even on windows. When you pass in a single name, the function will return the raw bytes of that file or None if the name
|
||||
was not found in the zip file. If you pass in more than one name then it returns a dict mapping the names to bytes.
|
||||
If a name is not found, it will not be present in the returned dict.
|
||||
|
||||
**get_icons(name_or_list_of_names)**
|
||||
A convenience wrapper for get_resources() that creates QIcon objects from the raw bytes returned by get_resources.
|
||||
If a name is not found in the zip file the corresponding QIcon will be null.
|
||||
|
||||
|
||||
The different types of plugins
|
||||
--------------------------------
|
||||
|
||||
As you may have noticed above, a plugin in |app| is a class. There are different classes for the different types of plugins in |app|.
|
||||
Details on each class, including the base class of all plugins can be found in :ref:`plugins`.
|
||||
|
||||
More plugin examples
|
||||
----------------------
|
||||
|
||||
You can find a list of many, sophisticated |app| plugins `here <http://www.mobileread.com/forums/showthread.php?t=118764>`_.
|
||||
|
||||
Sharing your plugins with others
|
||||
----------------------------------
|
||||
|
||||
If you would like to share the plugins you have created with other users of |app|, post your plugin in a new thread in the
|
||||
`calibre plugins forum <http://www.mobileread.com/forums/forumdisplay.php?f=237>`_.
|
||||
|
||||
|
@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
# The class that all interface action plugins must inherit from
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
|
||||
if False:
|
||||
# This is here to keep my python error checker from complaining about
|
||||
# the builtins that will be defined by the plugin loading system
|
||||
get_icons = None
|
||||
|
||||
class InterfacePlugin(InterfaceAction):
|
||||
|
||||
name = 'Interface Plugin Demo'
|
||||
|
||||
action_spec = ('Interface Plugin Demo', None,
|
||||
'Run the Interface Plugin Demo', 'Ctrl+Shift+F1')
|
||||
|
||||
def genesis(self):
|
||||
# This method is called once per plugin, do initial setup here
|
||||
print (1111, get_icons('icon.png'))
|
||||
self.qaction.setIcon(get_icons('icon.png'))
|
||||
|
@ -27,7 +27,8 @@ class InterfacePluginDemo(InterfaceActionBase):
|
||||
version = (1, 0, 0)
|
||||
minimum_calibre_version = (0, 7, 51)
|
||||
|
||||
#: This field defines the plugin class that contains all the code
|
||||
#: that actually does something.
|
||||
actual_plugin = 'calibre_plugins.interface.ui:InterfacePlugin'
|
||||
#: 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.interface_demo.ui:InterfacePlugin'
|
||||
|
@ -0,0 +1,7 @@
|
||||
The Interface Plugin Demo
|
||||
===========================
|
||||
|
||||
Created by Kovid Goyal
|
||||
|
||||
Requires calibre >= 0.7.52
|
||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
102
src/calibre/manual/plugin_examples/interface_demo/main.py
Normal file
102
src/calibre/manual/plugin_examples/interface_demo/main.py
Normal file
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox
|
||||
|
||||
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
|
||||
get_icons = get_resources = None
|
||||
|
||||
class DemoDialog(QDialog):
|
||||
|
||||
def __init__(self, gui, icon):
|
||||
QDialog.__init__(self, gui)
|
||||
self.gui = gui
|
||||
|
||||
# The current database shown in the GUI
|
||||
# db is an instance of the class LibraryDatabase2 from database.py
|
||||
# This class has many, many methods that allow you to do a lot of
|
||||
# things.
|
||||
self.db = gui.current_db
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
|
||||
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.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):
|
||||
fmt_idx = self.db.FIELD_MAP['formats']
|
||||
matched_ids = set()
|
||||
for record in self.db.data.iterall():
|
||||
# Iterate over all records
|
||||
fmts = record[fmt_idx]
|
||||
# fmts is either None or a comma separated list of formats
|
||||
if fmts and ',' not in fmts:
|
||||
matched_ids.add(record[0])
|
||||
# Mark the records with the matching ids
|
||||
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):
|
||||
most_recent = most_recent_id = None
|
||||
timestamp_idx = self.db.FIELD_MAP['timestamp']
|
||||
|
||||
for record in self.db.data:
|
||||
# Iterate over all currently showing records
|
||||
timestamp = record[timestamp_idx]
|
||||
if most_recent is None or timestamp > most_recent:
|
||||
most_recent = timestamp
|
||||
most_recent_id = record[0]
|
||||
|
||||
if most_recent_id is not None:
|
||||
# Get the row number of the id as shown in the GUI
|
||||
row_number = self.db.row(most_recent_id)
|
||||
# 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_books([row_number])
|
||||
|
||||
|
58
src/calibre/manual/plugin_examples/interface_demo/ui.py
Normal file
58
src/calibre/manual/plugin_examples/interface_demo/ui.py
Normal file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
# The class that all interface action plugins must inherit from
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre_plugins.interface_demo.main import DemoDialog
|
||||
|
||||
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
|
||||
get_icons = get_resources = None
|
||||
|
||||
class InterfacePlugin(InterfaceAction):
|
||||
|
||||
name = 'Interface 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 = ('Interface Plugin Demo', None,
|
||||
'Run the Interface Plugin Demo', 'Ctrl+Shift+F1')
|
||||
|
||||
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):
|
||||
# self.gui is the main calibre GUI. It acts as the gateway to access
|
||||
# all the elements of the calibre user interface, it should also be the
|
||||
# parent of the dialog
|
||||
d = DemoDialog(self.gui, self.qaction.icon())
|
||||
d.show()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user