Grand Sorcerer
Posts: 6,636
Karma: 12595249
Join Date: Jun 2009
Location: Madrid, Spain
Device: Kobo Clara/Aura One/Forma,XiaoMI 5, iPad, Huawei MediaPad, YotaPhone 2
|
Quote:
Originally Posted by thiago.eec
My modules:
- Show all editor actions [module name: editor_actions](by @capink)
Spoiler:
Code:
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~
from __future__ import (unicode_literals, division, absolute_import,
print_function)
import re
# python 3 compatibility
from six import text_type as unicode
from PyQt5.Qt import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout,
QDialog, QDialogButtonBox, QMenu, QPainter, QPoint, QPixmap,
QSize, QIcon, QTreeWidgetItem, QTreeWidget, QAbstractItemView,
QGroupBox, QCheckBox, QLabel, QAction)
from calibre import prints
from calibre.constants import DEBUG
from calibre.gui2 import error_dialog
from calibre_plugins.editor_chains.actions.base import EditorAction
from calibre_plugins.editor_chains.common_utils import get_icon
try:
load_translations()
except NameError:
prints("EditorChains::actions/editor_actions.py - exception when loading translations")
EXCLUDED_GROUPS = [
_('Editor Chains'),
_('Editor actions'),
_('Windows'),
_('Preview'),
_('Search')
]
ICON_SIZE = 32
def group_unique_names(gui, group):
menu_actions = [x for x in gui.__dict__.values() if isinstance(x, QAction)]
menu_unique_names = [re.sub(r'^action\-', r'', x.objectName()) for x in menu_actions]
if group == 'Plugins':
unique_names = [x for x in gui.keyboard.groups['Plugins']]
else:
unique_names = [x for x in gui.keyboard.groups[group] if x in menu_unique_names]
return unique_names
def get_qaction_from_unique_name(gui, unique_name):
shortcut = gui.keyboard.shortcuts[unique_name]
ac = shortcut['action']
return ac
def allowed_unique_names(gui):
unique_names = []
for group in gui.keyboard.groups.keys():
unique_names.extend(group_unique_names(gui, group))
unique_names += [x for x in gui.keyboard.groups['Plugins']]
return unique_names
class Item(QTreeWidgetItem):
pass
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.plugin_action = plugin_action
self.gui = plugin_action.gui
self.all_nodes = {}
# used to keep track of selected item and allow only one selection
self.checked_item = None
self._init_controls()
def _init_controls(self):
self.blank_icon = QIcon(I('blank.png'))
l = self.l = QVBoxLayout()
self.setLayout(l)
# opts_groupbox = QGroupBox()
# l.addWidget(opts_groupbox)
# opts_groupbox_layout = QVBoxLayout()
# opts_groupbox.setLayout(opts_groupbox_layout)
# cursor_chk = self.cursor_chk = QCheckBox(_('Disable wait cursor for this action.'))
# opts_groupbox_layout.addWidget(cursor_chk)
# cursor_chk.setChecked(False)
self.tv = QTreeWidget(self.gui)
self.tv.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.tv.header().hide()
self.tv.setSelectionMode(QAbstractItemView.SingleSelection)
l.addWidget(self.tv, 1)
self.tv.itemChanged.connect(self._tree_item_changed)
self._populate_actions_tree()
def _get_scaled_icon(self, icon):
if icon.isNull():
return self.blank_icon
# We need the icon scaled to 16x16
src = icon.pixmap(ICON_SIZE, ICON_SIZE)
if src.width() == ICON_SIZE and src.height() == ICON_SIZE:
return icon
# Need a new version of the icon
pm = QPixmap(ICON_SIZE, ICON_SIZE)
pm.fill(Qt.transparent)
p = QPainter(pm)
p.drawPixmap(QPoint(int((ICON_SIZE - src.width()) / 2), int((ICON_SIZE - src.height()) / 2)), src)
p.end()
return QIcon(pm)
def set_checked_state(self, item, col, state):
item.setCheckState(col, state)
if state == Qt.Checked:
self.checked_item = item
else:
self.checked_item = None
def _populate_actions_tree(self):
self.top_level_items_map = {}
for group in sorted(self.gui.keyboard.groups.keys()):
if group in EXCLUDED_GROUPS: continue
# Create a node for our top level plugin name
tl = Item()
tl.setText(0, group)
# Normal top-level checkable plugin iaction handling
tl.setFlags(Qt.ItemIsEnabled)
#unique_names = self.gui.keyboard.groups[group]
unique_names = group_unique_names(self.gui, group)
# Iterate through all the children of this node
self._populate_action_children(unique_names, tl)
self.tv.addTopLevelItem(tl)
def _populate_action_children(self, unique_names, parent):
for unique_name in unique_names:
ac = get_qaction_from_unique_name(self.gui, unique_name)
text = ac.text().replace('&', '')
it = Item(parent)
it.setText(0, text)
it.setData(0, Qt.UserRole, unique_name)
it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
it.setCheckState(0, Qt.Unchecked)
it.setIcon(0, self._get_scaled_icon(ac.icon()))
self.all_nodes[unique_name] = it
def _tree_item_changed(self, item, column):
# Checkstate has been changed
is_checked = item.checkState(column) == Qt.Checked
#text = unicode(item.text(column)).replace('&', '')
unique_name = item.data(column, Qt.UserRole)
if is_checked:
# un-check previously checked item if any
if self.checked_item:
self.tv.blockSignals(True)
self.checked_item.setCheckState(0, Qt.Unchecked)
self.tv.blockSignals(False)
self.checked_item = item
else:
self.checked_item = None
def _repopulate_actions_tree(self):
self.tv.clear()
self._populate_actions_tree()
def load_settings(self, settings):
if settings:
#self.cursor_chk.setChecked(settings.get('disable_busy_cursor', False))
#self._repopulate_actions_tree()
self.set_unique_name_checked(settings['unique_name'])
def save_settings(self):
settings = {}
settings['unique_name'] = self.checked_item.data(0, Qt.UserRole)
#settings['disable_busy_cursor'] = self.cursor_chk.isChecked()
return settings
def set_unique_name_checked(self, unique_name):
it = self.all_nodes.get(unique_name)
if it:
self.set_checked_state(it, 0, Qt.Checked)
self.tv.setCurrentItem(it)
class EditorActions(EditorAction):
name = 'Editor Actions'
#_is_builtin = True
def run(self, chain, settings):
ac = get_qaction_from_unique_name(chain.gui, settings['unique_name'])
if ac:
if DEBUG:
prints('Editor Chains: Editor Actions: Found action: {}'.format(ac.text().replace('&', '')))
cursor = Qt.WaitCursor
if settings.get('disable_busy_cursor'):
cursor = Qt.ArrowCursor
QApplication.setOverrideCursor(cursor)
try:
ac.trigger()
finally:
QApplication.restoreOverrideCursor()
if DEBUG:
prints('Editor Chains: Editor Actions: Finishing run action')
def validate(self, settings):
gui = self.plugin_action.gui
if not settings:
return (_('Settings Error'), _('You must configure this action before running it'))
if settings['unique_name'] not in allowed_unique_names(gui):
return (_('Settings Error'), _('Action cannot be found: {}'.format(settings['unique_name'])))
return True
def config_widget(self):
return ConfigWidget
- Run 'Check book' and fix all fixable errors [module name: fix_all]
- Merge files [module name: merge_files](by @capink)
Spoiler:
Code:
import re
from qt.core import (QWidget, QVBoxLayout, QGroupBox)
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
from calibre.ebooks.oeb.polish.split import merge
from calibre.ebooks.oeb.polish.replace import rename_files
from calibre.gui2.tweak_book import editors
from calibre_plugins.editor_chains.actions.base import EditorAction
from calibre_plugins.editor_chains.scope import scope_names, ScopeWidget, validate_scope, scope_is_headless
def spine_index(container, name):
idx = 0
for spine_name, is_linear in container.spine_names:
idx +=1
if name == spine_name:
return idx
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.plugin_action = plugin_action
self._init_controls()
def _init_controls(self):
l = self.l = QVBoxLayout()
self.setLayout(l)
scope_groupbox = QGroupBox(_('Files to merge'))
l.addWidget(scope_groupbox)
scope_l = QVBoxLayout()
scope_groupbox.setLayout(scope_l)
self.where_box = ScopeWidget(self, self.plugin_action, orientation='vertical',
headless=self.plugin_action.gui is None,
hide=['selected-text','open','current'])
scope_l.addWidget(self.where_box)
l.addStretch(1)
self.resize(self.sizeHint())
def load_settings(self, settings):
if settings:
self.where_box.where = settings['where']
def save_settings(self):
settings = {}
settings['where'] = self.where_box.where
return settings
class MergeFiles(EditorAction):
name = 'Merge Files'
headless = True
def run(self, chain, settings, *args, **kwargs):
names = scope_names(chain, settings['where'])
print(f'DEBUG: Merging Files\n names: {names}')
if len(names) < 2:
print('Error: You need to select at least 2 files to merge')
return
container = chain.current_container
spine_names = [name for name, is_linear in container.spine_names]
if names[0] in set(container.manifest_items_of_type(OEB_DOCS)):
category = 'text'
elif names[0] in set(container.manifest_items_of_type(OEB_STYLES)):
category = 'styles'
else:
print('Error: Can only merge files of categories: text or styles')
return
if category == 'text':
if not set(names).issubset(set(container.manifest_items_of_type(OEB_DOCS))):
print('Error: some files are your trying to merge are not of category text')
return
if not set(names).issubset(set(spine_names)):
print('Error: Some files you are trying to merge not in spine')
return
names = sorted(names, key=lambda name: spine_index(container, name))
else:
if not set(names).issubset(set(container.manifest_items_of_type(OEB_STYLES))):
print('Error: some files are your trying to merge are not of category styles')
return
names = sorted(names)
master = names[0]
print(f'DEBUG: Merging Files\n master: {master}')
# Create groups with the same prefix before merging
while names:
master = names[0]
group = []
for name in names[:]:
master_prefix = re.search('.*?_split_[^d+]', master)[0]
if re.search('.*?_split_[^d+]', name)[0] == master_prefix:
group.append(name)
names.remove(name)
merge(container, category, group, master)
new_name = re.sub('_split_.*?\d+', '', master)
rename_files(container, {master: new_name})
#if new_name in editors:
# chain.boss.show_editor(new_name)
def validate(self, settings):
scope_ok = validate_scope(settings['where'])
if scope_ok is not True:
return scope_ok
return True
def config_widget(self):
return ConfigWidget
def is_headless(self, settings):
return scope_is_headless(settings['where'])
- Pretty print all [module name: pretty_print_all]
Spoiler:
Code:
from qt.core import (Qt, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QCheckBox)
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
from calibre.ebooks.oeb.polish import pretty
from calibre_plugins.editor_chains.actions.base import EditorAction
def pretty_block(parent, settings, level=2, indent=' ', indent_level=1):
''' Surround block tags with blank lines and recurse into child block tags
that contain only other block tags '''
if not parent.text or pretty.isspace(parent.text):
parent.text = ''
if settings['block_spaces']:
nn = '\n' if hasattr(parent.tag, 'strip') and pretty.barename(parent.tag) in {'tr', 'td', 'th'} else '\n\n'
else:
nn = '\n'
parent.text = parent.text + nn + (indent * indent_level * level)
for i, child in enumerate(parent):
if pretty.isblock(child) and pretty.has_only_blocks(child):
pretty_block(child, settings, level=level+1, indent=indent * indent_level)
elif child.tag == pretty.SVG_TAG:
pretty_xml_tree(child, level=level, indent=indent, indent_level=indent_level)
l = level
if i == len(parent) - 1:
l -= 1
if not child.tail or pretty.isspace(child.tail):
child.tail = ''
child.tail = child.tail + nn + (indent * indent_level * l)
def pretty_xml_tree(elem, level=1, indent=' ', indent_level=1):
''' XML beautifier, assumes that elements that have children do not have
textual content. Also assumes that there is no text immediately after
closing tags. These are true for opf/ncx and container.xml files. If either
of the assumptions are violated, there should be no data loss, but pretty
printing won't produce optimal results.'''
if (not elem.text and len(elem) > 0) or (elem.text and pretty.isspace(elem.text)):
elem.text = '\n' + (indent * indent_level * (level+1))
for i, child in enumerate(elem):
pretty_xml_tree(child, level=level+1, indent=indent, indent_level=indent_level)
if not child.tail or pretty.isspace(child.tail):
l = level + 1
if i == len(elem) - 1:
l -= 1
child.tail = '\n' + (indent * indent_level * l)
def pretty_html_tree(container, root, settings):
indent = ' '
indent_level = settings['indent']
nn = '\n\n' if settings['root_spaces'] else '\n'
root.text = nn + indent * indent_level
for child in root:
if hasattr(child.tag, 'endswith') and child.tag.endswith('}head'):
pretty_xml_tree(child, indent_level=indent_level)
child.tail = nn + indent * indent_level
else:
child.tail = nn
for body in root.findall('h:body', namespaces=pretty.XPNSMAP):
pretty_block(body, settings, indent_level=indent_level)
# Special case the handling of a body that contains a single block tag
# with all content. In this case we prettify the containing block tag
# even if it has non block children.
if (len(body) == 1 and not callable(body[0].tag) and
pretty.isblock(body[0]) and
not pretty.has_only_blocks(body[0]) and
pretty.barename(body[0].tag) not in ('pre', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6') and
len(body[0]) > 0):
pretty_block(body[0], settings, level=2)
if container is not None:
# Handle <script> and <style> tags
for child in root.xpath('//*[local-name()="script" or local-name()="style"]'):
pretty.pretty_script_or_style(container, child)
def pretty_all(container, settings):
' Pretty print all HTML/CSS/XML files in the container '
xml_types = {pretty.guess_type('a.ncx'), pretty.guess_type('a.xml'), pretty.guess_type('a.svg')}
for name, mt in pretty.iteritems(container.mime_map):
prettied = False
if mt in OEB_DOCS:
pretty_html_tree(container, container.parsed(name), settings)
prettied = True
elif mt in OEB_STYLES:
container.parsed(name)
prettied = True
elif name == container.opf_name:
root = container.parsed(name)
pretty.pretty_opf(root)
pretty.pretty_xml_tree(root)
prettied = True
elif mt in xml_types:
pretty.pretty_xml_tree(container.parsed(name))
prettied = True
if prettied:
container.dirty(name)
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.plugin_action = plugin_action
self._init_controls()
def _init_controls(self):
l = self.l = QVBoxLayout()
self.setLayout(l)
h = self.h = QHBoxLayout()
l.addLayout(h)
self.label = QLabel(_('Indentation level:'))
h.addWidget(self.label)
self.indent_level = QSpinBox()
self.indent_level.setRange(1, 10)
h.addWidget(self.indent_level)
self.root_spaces = QCheckBox(_('Add spaces for head/body'))
l.addWidget(self.root_spaces)
self.block_spaces = QCheckBox(_('Add spaces between blocks'))
l.addWidget(self.block_spaces)
#l.addStretch(1)
#self.setMinimumSize(300,110)
self.resize(self.sizeHint())
def load_settings(self, settings):
if settings:
self.indent_level.setValue(settings['indent'])
self.root_spaces.setChecked(settings['root_spaces'])
self.block_spaces.setChecked(settings['block_spaces'])
def save_settings(self):
settings = {}
settings['indent'] = self.indent_level.value()
settings['root_spaces'] = self.root_spaces.isChecked()
settings['block_spaces'] = self.block_spaces.isChecked()
return settings
class BeautifyAll(EditorAction):
name = 'Pretty print ALL'
headless = True
def run(self, chain, settings, *args, **kwargs):
container = chain.current_container
QApplication.setOverrideCursor(Qt.WaitCursor)
QApplication.processEvents()
pretty_all(container, settings)
QApplication.restoreOverrideCursor()
def validate(self, settings):
if not settings:
return _('No settings'), _('You must first configure this action')
return True
def config_widget(self):
return ConfigWidget
- Replace invalid IDs [module name: replace_ids]
- Select files: this is used to convert Editor Chain's 'escope' to a selection [module name: select_files] (by @capink)
Spoiler:
Code:
import re
from qt.core import (QWidget, QVBoxLayout, QGroupBox)
from calibre_plugins.editor_chains.actions.base import EditorAction
from calibre_plugins.editor_chains.scope import scope_names, ScopeWidget, validate_scope, scope_is_headless
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.plugin_action = plugin_action
self._init_controls()
def _init_controls(self):
l = self.l = QVBoxLayout()
self.setLayout(l)
scope_groupbox = QGroupBox(_('Files to select'))
l.addWidget(scope_groupbox)
scope_l = QVBoxLayout()
scope_groupbox.setLayout(scope_l)
self.where_box = ScopeWidget(self, self.plugin_action, orientation='vertical',
headless=self.plugin_action.gui is None,
hide=['selected-text','open','current'])
scope_l.addWidget(self.where_box)
l.addStretch(1)
self.setMinimumSize(400,200)
def load_settings(self, settings):
if settings:
self.where_box.where = settings['where']
def save_settings(self):
settings = {}
settings['where'] = self.where_box.where
return settings
class SelectFiles(EditorAction):
name = 'Select Files'
headless = True
def run(self, chain, settings, *args, **kwargs):
names = scope_names(chain, settings['where'])
chain.gui.file_list.select_names(group)
def validate(self, settings):
scope_ok = validate_scope(settings['where'])
if scope_ok is not True:
return scope_ok
return True
def config_widget(self):
return ConfigWidget
def is_headless(self, settings):
return scope_is_headless(settings['where'])
- Upgrade to EPUB3 [module name: upgrade]
Spoiler:
Code:
from qt.core import (QApplication, Qt, QWidget, QVBoxLayout,
QCheckBox, QLabel)
from calibre import prints
from calibre.constants import DEBUG
from calibre.ebooks.oeb.polish.main import tweak_polish
from calibre.ebooks.oeb.polish import upgrade
from calibre_plugins.editor_chains.actions.base import EditorAction
try:
load_translations()
except NameError:
prints("EditorChains::actions/upgrade_internals.py - exception when loading translations")
def epub_2_to_3(container, previous_nav=None, remove_ncx=False, remove_guide=False):
upgrade.upgrade_metadata(container.opf)
upgrade.collect_properties(container)
toc = upgrade.get_toc(container)
toc_name = upgrade.find_existing_ncx_toc(container)
if toc_name and remove_ncx:
container.remove_item(toc_name)
container.opf_xpath('./opf:spine')[0].attrib.pop('toc', None)
landmarks = upgrade.get_landmarks(container)
if remove_guide:
for guide in container.opf_xpath('./opf:guide'):
guide.getparent().remove(guide)
upgrade.create_nav(container, toc, landmarks, previous_nav)
container.opf.set('version', '3.0')
if upgrade.fix_font_mime_types(container):
container.refresh_mime_map()
upgrade.migrate_obfuscated_fonts(container)
container.dirty(container.opf_name)
def upgrade_book(container, remove_ncx=False, remove_guide=False):
if container.book_type != 'epub' or container.opf_version_parsed.major >= 3:
return False
epub_2_to_3(container, remove_ncx=remove_ncx, remove_guide=remove_guide)
return True
class ConfigWidget(QWidget):
def __init__(self, plugin_action, chain_name, chains_config, *args, **kwargs):
QWidget.__init__(self)
self.plugin_action = plugin_action
self.chain_name = chain_name
self.chains_config = chains_config
self._init_controls()
def _init_controls(self):
l = self.l = QVBoxLayout()
self.setLayout(l)
self.remove_ncx = QCheckBox(_('Remove the legacy Table of Contents in NCX form'))
l.addWidget(self.remove_ncx)
self.remove_guide = QCheckBox(_('Remove the the OPF Guide element'))
l.addWidget(self.remove_guide)
l.addStretch(1)
self.resize(self.sizeHint())
def load_settings(self, settings):
if settings:
self.remove_ncx.setChecked(settings['remove_ncx'])
self.remove_guide.setChecked(settings['remove_guide'])
def save_settings(self):
settings = {}
settings['remove_ncx'] = self.remove_ncx.isChecked()
settings['remove_guide'] = self.remove_guide.isChecked()
return settings
class UpgradeInternals(EditorAction):
name = 'Upgrade to EPUB3'
_is_builtin_ = True
headless = True
def run(self, chain, settings, *args, **kwargs):
if settings is None:
return
upgrade_book(chain.current_container, settings['remove_ncx'], settings['remove_guide'])
def validate(self, settings):
if not settings:
return _('No settings'), _('You must first configure this action')
return True
def config_widget(self):
return ConfigWidget
The chains are attatched.
_______________________
Edit:
I noticed that the regex functions are not included with the chain. Most of them are used to convert items to pt-BR, so not relevant for most users. There is one, though, that is important:
Code:
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
if 'alt=' not in match[0]:
ans = match.group().replace(match.group(1), match.group(1) + ' alt=""')
else:
ans = match[0]
return ans
This is used for fixing missing 'alt' attributes, inside the 'Clean up' chain.
|
Thank you very much! With this, I'll be able to automate some of the options I do manually! And thanks to capink for the plugin and kovid for the program!
|