I have an alternative — incomplete — implementation for Calibre Actions (You can call it calibre actions 2), that exposes more actions, including toggling the cover grid.
Code:
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~
__license__ = 'GPL v3'
__copyright__ = '2021, Ahmed Zaki <azaki00.dev@gmail.com>'
__docformat__ = 'restructuredtext en'
import re
from qt.core 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.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.common_utils import get_icon
try:
load_translations()
except NameError:
prints("ActionChains::actions/calibre_actions2.py - exception when loading translations")
EXCLUDED_GROUPS = [
'Action Chains'
]
ICON_SIZE = 32
def get_qaction_from_unique_name(gui, unique_name):
if unique_name.startswith('location_manager'):
actions = gui.location_manager.all_actions[1:]
for ac in actions:
if 'location_manager:'+ac.calibre_name == unique_name:
return ac
else:
shortcut = gui.keyboard.shortcuts[unique_name]
ac = shortcut['action']
return ac
def all_unique_names(gui):
unique_names = []
for group in gui.keyboard.groups.keys():
unique_names.extend([x for x in gui.keyboard.groups[group]])
location_manager_actions = gui.location_manager.all_actions[1:]
location_manager_unique_names = ['location_manager:'+action.calibre_name for action in location_manager_actions]
unique_names += location_manager_unique_names
return unique_names
def validate(gui, settings, check_device_active=True):
if not settings:
return (_('Settings Error'), _('You must configure this action before running it'))
unique_name = settings['unique_name']
if unique_name not in all_unique_names(gui):
return (_('Settings Error'), _('Action cannot be found: {}'.format(unique_name)))
ac = get_qaction_from_unique_name(gui, unique_name)
if not ac:
return (_('Settings Error'), _('Menu entry: ({}) cannot be found'.format(unique_name)))
if unique_name.startswith('location_manager'):
if check_device_active:
if not gui.location_manager.has_device:
return (_('Device Error'), _('This action ({}) is only available when device view is active'.format(unique_name)))
return True
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)
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)
self.populate_location_manager()
def populate_location_manager(self):
# Create a node for our top level plugin name
tl = Item()
tl.setText(0, 'Location Manager')
# Normal top-level checkable plugin iaction handling
tl.setFlags(Qt.ItemIsEnabled)
actions = self.gui.location_manager.all_actions[1:]
unique_names = ['location_manager:'+action.calibre_name for action in actions]
# 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('&', '')
if not text:
text = unique_name
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 = str(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 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)
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 validate(self, settings):
gui = self.plugin_action.gui
return validate(gui, settings, check_device_active=False)
class CalibreActions(ChainAction):
name = 'Calibre Actions 2'
#_is_builtin = True
def run(self, gui, settings, chain):
unique_name = settings['unique_name']
if unique_name.startswith('location_manager'):
if not gui.location_manager.has_device:
if DEBUG:
prints('This action ({}) is only available when device view is active'.format(unique_name))
return
ac = get_qaction_from_unique_name(chain.gui, unique_name)
if ac:
if DEBUG:
prints('Action Chains: Calibre 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('Action Chains: Calibre Actions: Finishing run action')
def validate(self, settings):
gui = self.plugin_action.gui
return validate(gui, settings, check_device_active=True)
def config_widget(self):
return ConfigWidget
Note, this is an abandoned incomplete implementation which lacks some features compared to the builtin Calibre Actions:
- Device actions (under Location manager) are not implemented.
- Waiting for jobs and progress bars are not implemented.
So, better to always use the builtin Calibre Actions and only use this implementation for extra actions.
Note: you can find the toggle cover view grid action under the miscellaneous node.