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:
|
with ZipFile(f, 'w') as zf:
|
||||||
for x in os.listdir('.'):
|
for x in os.listdir('.'):
|
||||||
zf.write(x)
|
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'
|
bname = self.b(path) + '_plugin.zip'
|
||||||
subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS,
|
subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS,
|
||||||
bname)])
|
bname)])
|
||||||
|
@ -40,7 +40,8 @@ def get_resources(zfp, name_or_list_of_names):
|
|||||||
try:
|
try:
|
||||||
ans[name] = zf.read(name)
|
ans[name] = zf.read(name)
|
||||||
except:
|
except:
|
||||||
pass
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
if len(names) == 1:
|
if len(names) == 1:
|
||||||
ans = ans.pop(names[0], None)
|
ans = ans.pop(names[0], None)
|
||||||
|
|
||||||
@ -71,7 +72,7 @@ def get_icons(zfp, name_or_list_of_names):
|
|||||||
ians = {}
|
ians = {}
|
||||||
for name in names:
|
for name in names:
|
||||||
p = QPixmap()
|
p = QPixmap()
|
||||||
raw = ans.get('name', None)
|
raw = ans.get(name, None)
|
||||||
if raw:
|
if raw:
|
||||||
p.loadFromData(raw)
|
p.loadFromData(raw)
|
||||||
ians[name] = QIcon(p)
|
ians[name] = QIcon(p)
|
||||||
@ -136,8 +137,13 @@ class PluginLoader(object):
|
|||||||
raise ImportError('Plugin %r has no module named %r' %
|
raise ImportError('Plugin %r has no module named %r' %
|
||||||
(plugin_name, import_name))
|
(plugin_name, import_name))
|
||||||
with zipfile.ZipFile(zfp) as zf:
|
with zipfile.ZipFile(zfp) as zf:
|
||||||
code = zf.read(zinfo)
|
try:
|
||||||
compiled = compile(code, 'import_name', 'exec', dont_inherit=True)
|
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_resources'] = partial(get_resources, zfp)
|
||||||
mod.__dict__['get_icons'] = partial(get_icons, zfp)
|
mod.__dict__['get_icons'] = partial(get_icons, zfp)
|
||||||
exec compiled in mod.__dict__
|
exec compiled in mod.__dict__
|
||||||
@ -265,6 +271,9 @@ if __name__ == '__main__':
|
|||||||
if x[0] != '.':
|
if x[0] != '.':
|
||||||
print ('Adding', x)
|
print ('Adding', x)
|
||||||
zf.write(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)
|
add_plugin(f.name)
|
||||||
print ('Added plugin from', sys.argv[-1])
|
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
|
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
|
(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|.
|
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
|
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|.
|
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`.
|
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)
|
version = (1, 0, 0)
|
||||||
minimum_calibre_version = (0, 7, 51)
|
minimum_calibre_version = (0, 7, 51)
|
||||||
|
|
||||||
#: This field defines the plugin class that contains all the code
|
#: This field defines the GUI plugin class that contains all the code
|
||||||
#: that actually does something.
|
#: that actually does something. Its format is module_path:class_name
|
||||||
actual_plugin = 'calibre_plugins.interface.ui:InterfacePlugin'
|
#: 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