From 5dabfbd5499d63ced36156f7e1953174a9edadeb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 24 Jul 2014 18:48:45 +0530 Subject: [PATCH] Add documentation and an example editor plugin --- manual/creating_plugins.rst | 69 ++++++++++ .../plugin_examples/editor_demo/__init__.py | 19 +++ .../editor_demo/images/icon.png | Bin 0 -> 4248 bytes manual/plugin_examples/editor_demo/main.py | 123 ++++++++++++++++++ .../plugin-import-name-editor_plugin_demo.txt | 0 manual/plugins.rst | 10 ++ manual/polish.rst | 13 ++ src/calibre/gui2/tweak_book/boss.py | 35 ++++- src/calibre/gui2/tweak_book/plugin.py | 22 +++- src/calibre/gui2/tweak_book/preferences.py | 5 +- 10 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 manual/plugin_examples/editor_demo/__init__.py create mode 100644 manual/plugin_examples/editor_demo/images/icon.png create mode 100644 manual/plugin_examples/editor_demo/main.py create mode 100644 manual/plugin_examples/editor_demo/plugin-import-name-editor_plugin_demo.txt diff --git a/manual/creating_plugins.rst b/manual/creating_plugins.rst index fe58b6e34e..b595cd1611 100644 --- a/manual/creating_plugins.rst +++ b/manual/creating_plugins.rst @@ -59,6 +59,8 @@ and query the books database in |app|. You can download this plugin from `interface_demo_plugin.zip `_ +.. _import_name_txt: + 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``. @@ -180,6 +182,73 @@ You can see the ``prefs`` object being used in main.py: .. literalinclude:: plugin_examples/interface_demo/main.py :pyobject: DemoDialog.config + +Edit Book plugins +------------------------------------------ + +Now let's change gears for a bit and look at creating a plugin to add tools to +the |app| book editor. The plugin is available here: +`editor_demo_plugin.zip `_. + +The first step, as for all plugins is to create the +import name empty txt file, as described :ref:`above `. +We shall name the file ``plugin-import-name-editor_plugin_demo.txt``. + +Now we create the mandatory ``__init__.py`` file that contains metadata about +the plugin -- its name, author, version, etc. + +.. literalinclude:: plugin_examples/editor_demo/__init__.py + :lines: 8- + +A single editor plugin can provide multiple tools each tool corresponds to a +single button in the toolbar and entry in the :guilabel:`Plugins` menu in the +editor. These can have sub-menus in case the tool has multiple related actions. + +The tools must all be defined in the file ``main.py`` in your plugin. Every +tool is a class that inherits from the +:class:`calibre.gui2.tweak_book.plugin.Tool` class. Let's look at ``main.py`` +from the demo plugin, the source code is heavily commented and should be +self-explanatory. Read the API documents of the +:class:`calibre.gui2.tweak_book.plugin.Tool` class for more details. + +main.py +^^^^^^^^^ + +Here we will see the definition of a single tool that does a does a couple of +simple things that demonstrate the editor API most plugins will use. + +.. literalinclude:: plugin_examples/editor_demo/main.py + :lines: 8- + +Let's break down ``main.py``. We see that it defines a single tool, named +*Magnify fonts*. This tool will ask the user for a number and multiply all font +sizes in the book by that number. + +The first important thing is the tool name which you must set to some +relatively unique string as it will be used as the key for this tool. + +The next important entry point is the +:meth:`calibre.gui2.tweak_book.plugin.Tool.create_action`. This method creates +the QAction objects that appear in the plugins toolbar and plugin menu. +It also, optionally, assigns a keyboard shortcut that the user can customize. +The triggered signal from the QAction is connected to the ask_user() method +that asks the user for the font size multiplier, and then runs the +magnification code. + +The magnification code is well commented and fairly simple. The main things to +note are that you get a reference to the editor window as ``self.gui`` and the +editor *Boss* as ``self.boss``. The *Boss* is the object that controls the editor +user interface. It has many useful methods, that are documented in the +:class:`calibre.gui2.tweak_book.boss.Boss` class. + +Finally, there is ``self.current_container`` which is a reference to the book +being edited as a :class:`calibre.ebooks.oeb.polish.container.Container` +object. This represents the book as a collection of its constituent +HTML/CSS/image files and has convenience methods for doing many useful things. +The container object and various useful utility functions that can be reused in +your plugin code are documented in :ref:`polish_api`. + + Adding translations to your plugin -------------------------------------- diff --git a/manual/plugin_examples/editor_demo/__init__.py b/manual/plugin_examples/editor_demo/__init__.py new file mode 100644 index 0000000000..13fac4f6e4 --- /dev/null +++ b/manual/plugin_examples/editor_demo/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2014, Kovid Goyal ' + +from calibre.customize import EditBookToolPlugin + + +class DemoPlugin(EditBookToolPlugin): + + name = 'Edit Book plugin demo' + version = (1, 0, 0) + author = 'Kovid Goyal' + supported_platforms = ['windows', 'osx', 'linux'] + description = 'A demonstration of the plugin interface for the ebook editor' + minimum_calibre_version = (1, 46, 0) diff --git a/manual/plugin_examples/editor_demo/images/icon.png b/manual/plugin_examples/editor_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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 tags and style="" attributes + for name, media_type in container.mime_map.iteritems(): + if media_type in OEB_STYLES: + # A stylesheet. Parsed stylesheets are cssutils CSSStylesheet + # objects. + self.magnify_stylesheet(container.parsed(name), factor) + container.dirty(name) # Tell the container that we have changed the stylesheet + elif media_type in OEB_DOCS: + # A HTML file. Parsed HTML files are lxml elements + + for style_tag in container.parsed(name).xpath('//*[local-name="style"]'): + if style_tag.text and style_tag.get('type', None) in {None, 'text/css'}: + # We have an inline CSS