View Single Post
Old 02-04-2022, 11:35 AM   #823
capink
Wizard
capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.
 
Posts: 1,203
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
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.
capink is offline   Reply With Quote