mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add documentation and an example editor plugin
This commit is contained in:
parent
cde4bbb670
commit
5dabfbd549
@ -59,6 +59,8 @@ 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>`_
|
||||
|
||||
.. _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 <http://calibre-ebook.com/downloads/editor_demo_plugin.zip>`_.
|
||||
|
||||
The first step, as for all plugins is to create the
|
||||
import name empty txt file, as described :ref:`above <import_name_txt>`.
|
||||
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
|
||||
--------------------------------------
|
||||
|
||||
|
19
manual/plugin_examples/editor_demo/__init__.py
Normal file
19
manual/plugin_examples/editor_demo/__init__.py
Normal file
@ -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 <kovid at kovidgoyal.net>'
|
||||
|
||||
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)
|
BIN
manual/plugin_examples/editor_demo/images/icon.png
Normal file
BIN
manual/plugin_examples/editor_demo/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
123
manual/plugin_examples/editor_demo/main.py
Normal file
123
manual/plugin_examples/editor_demo/main.py
Normal file
@ -0,0 +1,123 @@
|
||||
#!/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 <kovid at kovidgoyal.net>'
|
||||
|
||||
import re
|
||||
from PyQt4.Qt import QAction, QInputDialog
|
||||
from cssutils.css import CSSRule
|
||||
|
||||
# The base class that all tools must inherit from
|
||||
from calibre.gui2.tweak_book.plugin import Tool
|
||||
|
||||
from calibre import force_unicode
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, serialize
|
||||
|
||||
class DemoTool(Tool):
|
||||
|
||||
#: Set this to a unique name it will be used as a key
|
||||
name = 'demo-tool'
|
||||
|
||||
#: If True the user can choose to place this tool in the plugins toolbar
|
||||
allowed_in_toolbar = True
|
||||
|
||||
#: If True the user can choose to place this tool in the plugins menu
|
||||
allowed_in_menu = True
|
||||
|
||||
def create_action(self, for_toolbar=True):
|
||||
# Create an action, this will be added to the plugins toolbar and
|
||||
# the plugins menu
|
||||
ac = QAction(get_icons('images/icon.png'), 'Magnify fonts', self.gui) # noqa
|
||||
if not for_toolbar:
|
||||
# Register a keyboard shortcut for this toolbar action. We only
|
||||
# register it for the action created for the menu, not the toolbar,
|
||||
# to avoid a double trigger
|
||||
self.register_shortcut(ac, 'magnify-fonts-tool', default_keys=('Ctrl+Shift+Alt+D',))
|
||||
ac.triggered.connect(self.ask_user)
|
||||
return ac
|
||||
|
||||
def ask_user(self):
|
||||
# Ask the user for a factor by which to multiply all font sizes
|
||||
factor, ok = QInputDialog.getDouble(
|
||||
self.gui, 'Enter a magnification factor', 'Allow font sizes in the book will be multiplied by the specified factor',
|
||||
value=2, min=0.1, max=4
|
||||
)
|
||||
if ok:
|
||||
# Ensure any in progress editing the user is doing is present in the container
|
||||
self.boss.commit_all_editors_to_container()
|
||||
try:
|
||||
self.magnify_fonts(factor)
|
||||
except Exception:
|
||||
# Something bad happened report the error to the user
|
||||
import traceback
|
||||
error_dialog(self.gui, _('Failed to magnify fonts'), _(
|
||||
'Failed to magnify fonts, click "Show details" for more info'),
|
||||
det_msg=traceback.format_exc(), show=True)
|
||||
# Revert to the saved restore point
|
||||
self.boss.revert_requested(self.boss.global_undo.previous_container)
|
||||
else:
|
||||
# Show the user what changes we have made, allowing her to
|
||||
# revert them if necessary
|
||||
self.boss.show_current_diff()
|
||||
# Update the editor UI to take into account all the changes we
|
||||
# have made
|
||||
self.boss.apply_container_update_to_gui()
|
||||
|
||||
def magnify_fonts(self, factor):
|
||||
# Magnify all font sizes defined in the book by the specified factor
|
||||
# First we create a restore point so that the user can undo all changes
|
||||
# we make.
|
||||
self.boss.add_savepoint('Before: Magnify fonts')
|
||||
|
||||
container = self.current_container # The book being edited as a container object
|
||||
|
||||
# Iterate over all style declarations int he book, this means css
|
||||
# stylesheets, <style> 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 <style> tag, parse it into a
|
||||
# stylesheet object
|
||||
sheet = container.parse_css(style_tag.text)
|
||||
self.magnify_stylesheet(sheet, factor)
|
||||
style_tag.text = serialize(sheet, 'text/css', pretty_print=True)
|
||||
container.dirty(name) # Tell the container that we have changed the stylesheet
|
||||
for elem in container.parsed(name).xpath('//*[@style]'):
|
||||
# Process inline style attributes
|
||||
block = container.parse_css(elem.get('style'), is_declaration=True)
|
||||
self.magnify_declaration(block, factor)
|
||||
elem.set('style', force_unicode(block.getCssText(separator=' '), 'utf-8'))
|
||||
|
||||
def magnify_stylesheet(self, sheet, factor):
|
||||
# Magnify all fonts in the specified stylesheet by the specified
|
||||
# factor.
|
||||
for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
|
||||
self.magnify_declaration(rule.style, factor)
|
||||
|
||||
def magnify_declaration(self, style, factor):
|
||||
# Magnify all fonts in the specified style declaration by the specified
|
||||
# factor
|
||||
val = style.getPropertyValue('font-size')
|
||||
if not val:
|
||||
return
|
||||
# see if the font-size contains a number
|
||||
num = re.search(r'[0-9.]+', val)
|
||||
if num is not None:
|
||||
num = num.group()
|
||||
val = val.replace(num, '%f' % (float(num) * factor))
|
||||
style.setProperty('font-size', val)
|
||||
# We should also be dealing with the font shorthand property and
|
||||
# font sizes specified as non numbers, but those are left as exercises
|
||||
# for the reader
|
@ -195,3 +195,13 @@ Viewer plugins
|
||||
:show-inheritance:
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
Edit Book plugins
|
||||
--------------------
|
||||
|
||||
.. autoclass:: calibre.gui2.tweak_book.plugin.Tool
|
||||
:show-inheritance:
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
|
@ -120,3 +120,16 @@ Working with the Table of Contents
|
||||
|
||||
.. autofunction:: create_inline_toc
|
||||
|
||||
|
||||
Controlling the editor's user interface
|
||||
-----------------------------------------
|
||||
|
||||
The ebook editor's user interface is controlled by a single global *Boss*
|
||||
object. This has many useful methods that can be used in plugin code to
|
||||
perform common tasks.
|
||||
|
||||
.. module:: calibre.gui2.tweak_book.boss
|
||||
|
||||
.. autoclass:: Boss
|
||||
:members:
|
||||
|
||||
|
@ -88,6 +88,7 @@ class Boss(QObject):
|
||||
self.ignore_preview_to_editor_sync = False
|
||||
setup_cssutils_serialization()
|
||||
_boss = self
|
||||
self.gui = parent
|
||||
|
||||
def __call__(self, gui):
|
||||
self.gui = gui
|
||||
@ -221,6 +222,11 @@ class Boss(QObject):
|
||||
self.gui.blocking_job('import_book', _('Importing book, please wait...'), self.book_opened, func, src, dest, tdir=self.mkdtemp())
|
||||
|
||||
def open_book(self, path=None, edit_file=None, clear_notify_data=True):
|
||||
'''
|
||||
Open the ebook at ``path`` for editing. Will show an error if the ebook is not in a supported format or the current book has unsaved changes.
|
||||
|
||||
:param edit_file: The name of a file inside the newly openend book to start editing.
|
||||
'''
|
||||
if not self._check_before_open():
|
||||
return
|
||||
if not hasattr(path, 'rpartition'):
|
||||
@ -314,6 +320,7 @@ class Boss(QObject):
|
||||
self.gui.file_list.build(container)
|
||||
|
||||
def apply_container_update_to_gui(self):
|
||||
' Update all the components of the user interface to reflect the latest data in the current book container '
|
||||
self.refresh_file_list()
|
||||
self.update_global_history_actions()
|
||||
self.update_editors_from_container()
|
||||
@ -581,6 +588,7 @@ class Boss(QObject):
|
||||
ac.setText(text + ' "%s"'%(getattr(gu, x + '_msg') or '...'))
|
||||
|
||||
def add_savepoint(self, msg):
|
||||
' Create a restore checkpoint with the name specified as ``msg`` '
|
||||
self.commit_all_editors_to_container()
|
||||
nc = clone_container(current_container(), self.mkdtemp())
|
||||
self.global_undo.add_savepoint(nc, msg)
|
||||
@ -588,6 +596,7 @@ class Boss(QObject):
|
||||
self.update_global_history_actions()
|
||||
|
||||
def rewind_savepoint(self):
|
||||
' Undo the previous creation of a restore checkpoint, useful if you create a checkpoint, then abort the operation with no changes '
|
||||
container = self.global_undo.rewind_savepoint()
|
||||
if container is not None:
|
||||
set_current_container(container)
|
||||
@ -613,6 +622,12 @@ class Boss(QObject):
|
||||
return d
|
||||
|
||||
def show_current_diff(self, allow_revert=True, to_container=None):
|
||||
'''
|
||||
Show the changes to the book from its last checkpointed state
|
||||
|
||||
:param allow_revert: If True the diff dialog will have a button to allow the user to revert all changes
|
||||
:param to_container: A container object to compare the current container to. If None, the previously checkpointed container is used
|
||||
'''
|
||||
self.commit_all_editors_to_container()
|
||||
d = self.create_diff_dialog()
|
||||
d.revert_requested.connect(partial(self.revert_requested, self.global_undo.previous_container))
|
||||
@ -644,6 +659,7 @@ class Boss(QObject):
|
||||
# }}}
|
||||
|
||||
def set_modified(self):
|
||||
' Mark the book as having been modified '
|
||||
self.gui.action_save.setEnabled(True)
|
||||
|
||||
def fix_html(self, current):
|
||||
@ -750,7 +766,7 @@ class Boss(QObject):
|
||||
self.gui.central.pre_fill_search(text)
|
||||
|
||||
def search(self, action, overrides=None):
|
||||
' Run a search/replace '
|
||||
# Run a search/replace
|
||||
sp = self.gui.central.search_panel
|
||||
# Ensure the search panel is visible
|
||||
sp.setVisible(True)
|
||||
@ -767,13 +783,13 @@ class Boss(QObject):
|
||||
self.gui, self.show_editor, self.edit_file, self.show_current_diff, self.add_savepoint, self.rewind_savepoint, self.set_modified)
|
||||
|
||||
def find_word(self, word, locations):
|
||||
' Go to a word from the spell check dialog '
|
||||
# Go to a word from the spell check dialog
|
||||
ed = self.gui.central.current_editor
|
||||
name = editor_name(ed)
|
||||
find_next_word(word, locations, ed, name, self.gui, self.show_editor, self.edit_file)
|
||||
|
||||
def next_spell_error(self):
|
||||
' Go to the next spelling error '
|
||||
# Go to the next spelling error
|
||||
ed = self.gui.central.current_editor
|
||||
name = editor_name(ed)
|
||||
find_next_error(ed, name, self.gui, self.show_editor, self.edit_file)
|
||||
@ -855,6 +871,9 @@ class Boss(QObject):
|
||||
self.gui.file_list.build(container)
|
||||
|
||||
def commit_all_editors_to_container(self):
|
||||
''' Commit any changes that the user has made to files open in editors to
|
||||
the container. You should call this method before performing any
|
||||
actions on the current container '''
|
||||
changed = False
|
||||
with BusyCursor():
|
||||
for name, ed in editors.iteritems():
|
||||
@ -865,6 +884,7 @@ class Boss(QObject):
|
||||
return changed
|
||||
|
||||
def save_book(self):
|
||||
' Save the book. Saving is performed in the background '
|
||||
c = current_container()
|
||||
for name, ed in editors.iteritems():
|
||||
if ed.is_modified or not ed.is_synced_to_container:
|
||||
@ -1094,6 +1114,7 @@ class Boss(QObject):
|
||||
self.ignore_preview_to_editor_sync = False
|
||||
|
||||
def sync_preview_to_editor(self):
|
||||
' Sync the position of the preview panel to the current cursor position in the current editor '
|
||||
if self.ignore_preview_to_editor_sync:
|
||||
return
|
||||
ed = self.gui.central.current_editor
|
||||
@ -1103,6 +1124,7 @@ class Boss(QObject):
|
||||
self.gui.preview.sync_to_editor(name, ed.current_tag())
|
||||
|
||||
def sync_live_css_to_editor(self):
|
||||
' Sync the Live CSS panel to the current cursor position in the current editor '
|
||||
ed = self.gui.central.current_editor
|
||||
if ed is not None:
|
||||
name = editor_name(ed)
|
||||
@ -1135,6 +1157,11 @@ class Boss(QObject):
|
||||
self.gui.central.add_editor(name, editor)
|
||||
|
||||
def edit_file(self, name, syntax=None, use_template=None):
|
||||
''' Open the file specified by name in an editor
|
||||
|
||||
:param syntax: The media type of the file, for example, ``'text/html'``. If not specified it is guessed from the file extension.
|
||||
:param use_template: A template to initialize the opened editor with
|
||||
'''
|
||||
editor = editors.get(name, None)
|
||||
if editor is None:
|
||||
syntax = syntax or syntax_from_mime(name, guess_type(name))
|
||||
@ -1156,6 +1183,7 @@ class Boss(QObject):
|
||||
return editor
|
||||
|
||||
def show_editor(self, name):
|
||||
' Show the editor that is editing the file specified by ``name`` '
|
||||
self.gui.central.show_editor(editors[name])
|
||||
editors[name].set_focus()
|
||||
|
||||
@ -1270,6 +1298,7 @@ class Boss(QObject):
|
||||
self.close_editor(name)
|
||||
|
||||
def close_editor(self, name):
|
||||
' Close the editor that is editing the file specified by ``name`` '
|
||||
editor = editors.pop(name)
|
||||
self.gui.central.close_editor(editor)
|
||||
editor.break_cycles()
|
||||
|
@ -12,7 +12,7 @@ from PyQt4.Qt import QToolButton
|
||||
|
||||
from calibre import prints
|
||||
from calibre.customize.ui import all_edit_book_tool_plugins
|
||||
from calibre.gui2.tweak_book import tprefs
|
||||
from calibre.gui2.tweak_book import tprefs, current_container
|
||||
from calibre.gui2.tweak_book.boss import get_boss
|
||||
|
||||
class Tool(object):
|
||||
@ -39,6 +39,11 @@ class Tool(object):
|
||||
' The main window of the user interface '
|
||||
return self.boss.gui
|
||||
|
||||
@property
|
||||
def current_container(self):
|
||||
' Return the current :class:`calibre.ebooks.oeb.polish.container.Container` object that represents the book being edited. '
|
||||
return current_container()
|
||||
|
||||
def register_shortcut(self, qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data):
|
||||
'''
|
||||
Register a keyboard shortcut that will trigger the specified ``qaction``. This keyboard shortcut
|
||||
@ -103,12 +108,17 @@ def load_plugin_tools(plugin):
|
||||
def plugin_action_sid(plugin, tool, for_toolbar=True):
|
||||
return plugin.name + tool.name + ('toolbar' if for_toolbar else 'menu')
|
||||
|
||||
plugin_toolbar_actions = []
|
||||
|
||||
def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_actions=None, plugin_menu_actions=None):
|
||||
try:
|
||||
ac = tool.create_action(for_toolbar=for_toolbar)
|
||||
if ac is None:
|
||||
raise RuntimeError('create_action() failed to return an action')
|
||||
except Exception:
|
||||
prints('Failed to create action for tool:', tool.name)
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
traceback.print_exc()
|
||||
return
|
||||
sid = plugin_action_sid(plugin, tool, for_toolbar)
|
||||
if actions is not None and sid in actions:
|
||||
@ -120,6 +130,7 @@ def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_action
|
||||
if for_toolbar:
|
||||
if toolbar_actions is not None:
|
||||
toolbar_actions[sid] = ac
|
||||
plugin_toolbar_actions.append(ac)
|
||||
ac.popup_mode = {'instant':QToolButton.InstantPopup, 'button':QToolButton.MenuButtonPopup}.get(
|
||||
tool.toolbar_button_popup_mode, QToolButton.DelayedPopup)
|
||||
else:
|
||||
@ -127,9 +138,15 @@ def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_action
|
||||
plugin_menu_actions.append(ac)
|
||||
return ac
|
||||
|
||||
_tool_memory = [] # Needed to prevent the tool object from being garbage collected
|
||||
|
||||
def create_plugin_actions(actions, toolbar_actions, plugin_menu_actions):
|
||||
del _tool_memory[:]
|
||||
del plugin_toolbar_actions[:]
|
||||
|
||||
for plugin in all_edit_book_tool_plugins():
|
||||
for tool in load_plugin_tools(plugin):
|
||||
_tool_memory.append(tool)
|
||||
if tool.allowed_in_toolbar:
|
||||
create_plugin_action(plugin, tool, True, actions, toolbar_actions, plugin_menu_actions)
|
||||
if tool.allowed_in_menu:
|
||||
@ -141,3 +158,4 @@ def install_plugin(plugin):
|
||||
sid = plugin_action_sid(plugin, tool, True)
|
||||
if sid not in tprefs['global_plugins_toolbar']:
|
||||
tprefs['global_plugins_toolbar'] = tprefs['global_plugins_toolbar'] + [sid]
|
||||
|
||||
|
@ -400,13 +400,16 @@ class ToolbarSettings(QWidget):
|
||||
return unicode(self.bars.itemData(self.bars.currentIndex()).toString())
|
||||
|
||||
def build_lists(self):
|
||||
from calibre.gui2.tweak_book.plugin import plugin_toolbar_actions
|
||||
self.available.clear(), self.current.clear()
|
||||
name = self.current_name
|
||||
if not name:
|
||||
return
|
||||
items = self.current_settings[name]
|
||||
applied = set(items)
|
||||
if name.startswith('global_'):
|
||||
if name == 'global_plugins_toolbar':
|
||||
all_items = {x.sid:x for x in plugin_toolbar_actions}
|
||||
elif name.startswith('global_'):
|
||||
all_items = toolbar_actions
|
||||
elif name == 'editor_common_toolbar':
|
||||
all_items = {x:actions[x] for x in tprefs.defaults[name] if x}
|
||||
|
Loading…
x
Reference in New Issue
Block a user