Complete the example plugin and plugin creating tutorial

This commit is contained in:
Kovid Goyal 2011-03-27 11:00:54 -06:00
parent 288905ee34
commit 03cc260fe5
10 changed files with 286 additions and 38 deletions

View File

@ -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)])

View File

@ -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:
code = zf.read(zinfo)
compiled = compile(code, 'import_name', 'exec', dont_inherit=True)
try:
code = zf.read(zinfo)
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])

View File

@ -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>`_.

View File

@ -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'))

View File

@ -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'

View File

@ -0,0 +1,7 @@
The Interface Plugin Demo
===========================
Created by Kovid Goyal
Requires calibre >= 0.7.52

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View 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])

View 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()