View Single Post
Old 03-18-2021, 09:14 AM   #408
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,212
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
This custom action seems to be working even when the tag browser has no focus and also when it is hidden

Code:
# python3 compatibility
from six.moves import range
from six import text_type as unicode

from PyQt5.Qt import (QApplication, Qt, QWidget, QVBoxLayout, QHBoxLayout,
                      QGroupBox, QTableWidget, QComboBox,  QToolButton,
                      QIcon, QSpacerItem, QSizePolicy, QAbstractItemView)

from calibre import prints
from calibre.constants import DEBUG, numeric_version
from calibre.gui2 import question_dialog

from calibre_plugins.action_chains.actions.base import ChainAction

class CategoryComboBox(QComboBox):

    def __init__(self, parent, categories, selected_text=None):
        QComboBox.__init__(self, parent)
        self.populate_combo(categories, selected_text)

    def populate_combo(self, categories, selected_text=None):
        self.blockSignals(True)
        self.clear()
        for category in categories:
            self.addItem(category)
        self.select_cateogry(selected_text)

    def select_cateogry(self, selected_text):
        self.blockSignals(True)
        if selected_text:
            idx = self.findText(selected_text)
            if idx == -1:
                self.addItem(selected_text)
                idx = self.findText(selected_text)
            self.setCurrentIndex(idx)
        else:
            self.setCurrentIndex(-1)
        self.blockSignals(False)

class CategoriesTable(QTableWidget):

    def __init__(self, plugin_action, data_items=[]):
        QTableWidget.__init__(self)
        self.plugin_action = plugin_action
        self.db = self.plugin_action.gui.current_db
        tag_browser = self.plugin_action.gui.tags_view
        self.all_categories = [ category.category_key for category \
                    in tag_browser._model.category_nodes if category.category_key.find('.') == -1 ]
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.setAlternatingRowColors(True)
        self.populate_table(data_items)

    def populate_table(self, data_items):
        self.clear()
        self.setRowCount(len(data_items))
        header_labels = [_('Category'), _('Operation'), '']
        self.setColumnCount(len(header_labels))
        self.setHorizontalHeaderLabels(header_labels)
        self.verticalHeader().setDefaultSectionSize(24)

        for row, data in enumerate(data_items):
            self.populate_table_row(row, data)

        self.resizeColumnsToContents()

        for col in range(self.columnCount()):
            self.setMinimumColumnWidth(col, 150)        

        self.setSortingEnabled(False)
        self.setMinimumSize(800, 0)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.selectRow(0)

    def populate_table_row(self, row, data):
        self.blockSignals(True)
        category_combo = CategoryComboBox(self, self.all_categories, data['category_key'])        
        operation_combo = CategoryComboBox(self, ['toggle','show','hide'], data['operation'])
        self.setCellWidget(row, 0, category_combo)
        self.setCellWidget(row, 1, operation_combo)
        self.blockSignals(False)

    def setMinimumColumnWidth(self, col, minimum):
        if self.columnWidth(col) < minimum:
            self.setColumnWidth(col, minimum)

    def add_data(self, data_items, append=False):
        if not append:
            self.setRowCount(0)
        for data in reversed(data_items):
            row = self.currentRow() + 1
            self.insertRow(row)
            self.populate_table_row(row, data)

    def get_data(self):
        data_items = []
        for row in range(self.rowCount()):
            data_item = self.convert_row_to_data(row)
            # filter out empty or incomplete rows
            if not (data_item['category_key'] and data_item['operation']):
                continue
            data_items.append(data_item)
        return data_items

    def convert_row_to_data(self, row):
        data = {}
        category_combo = self.cellWidget(row, 0)
        data['category_key'] = unicode(category_combo.currentText()).strip()
        operation_combo = self.cellWidget(row, 1)
        data['operation'] = unicode(operation_combo.currentText()).strip()
        return data      

    def create_blank_row_data(self):
        data = {}
        data['category_key'] = ''
        data['operation'] = ''
        return data

    def select_and_scroll_to_row(self, row):
        self.selectRow(row)
        self.scrollToItem(self.currentItem())

    def add_row(self):
        self.setFocus()
        # We will insert a blank row below the currently selected row
        row = self.currentRow() + 1
        self.insertRow(row)
        self.populate_table_row(row, self.create_blank_row_data())
        self.select_and_scroll_to_row(row)

    def delete_rows(self):
        self.setFocus()
        rows = self.selectionModel().selectedRows()
        rows = sorted(rows, key=lambda x: x.row())
        if len(rows) == 0:
            return
        message = '<p>Are you sure you want to delete this row?'
        if len(rows) > 1:
            message = '<p>Are you sure you want to delete the selected %d rows?'%len(rows)
        if not question_dialog(self, _('Are you sure?'), message, show_copy_button=False):
            return
        first_sel_row = self.currentRow()
        for selrow in reversed(rows):
            self.removeRow(selrow.row())
        if first_sel_row < self.rowCount():
            self.select_and_scroll_to_row(first_sel_row)
        elif self.rowCount() > 0:
            self.select_and_scroll_to_row(first_sel_row - 1)

class ConfigWidget(QWidget):

    def __init__(self, plugin_action):
        QWidget.__init__(self)
        self.plugin_action = plugin_action
        self.gui = plugin_action.gui
        self.db = self.gui.current_db
        self._init_controls()

    def _init_controls(self):

        layout = QVBoxLayout()
        self.setLayout(layout)

        categories_group_box = QGroupBox(_('Category Operations'))
        layout.addWidget(categories_group_box)
        categories_group_box_layout = QHBoxLayout()
        categories_group_box.setLayout(categories_group_box_layout)
        self.table = CategoriesTable(self)
        categories_group_box_layout.addWidget(self.table)

        # Add a vertical layout containing the the buttons to move up/down etc.
        button_layout = QVBoxLayout()
        categories_group_box_layout.addLayout(button_layout)

        add_button = QToolButton(self)
        add_button.setToolTip(_('Add row'))
        add_button.setIcon(QIcon(I('plus.png')))
        button_layout.addWidget(add_button)
        spacerItem1 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        button_layout.addItem(spacerItem1)

        delete_button = QToolButton(self)
        delete_button.setToolTip(_('Delete row'))
        delete_button.setIcon(QIcon(I('minus.png')))
        button_layout.addWidget(delete_button)

        add_button.clicked.connect(self.table.add_row)
        delete_button.clicked.connect(self.table.delete_rows)
        
        layout.addStretch(1)

        self.setMinimumSize(600,300)

    def load_settings(self, settings):
        categories_config = settings.get('categories_config', [])
        self.table.add_data(categories_config)

    def save_settings(self):
        settings = {}
        settings['categories_config'] = self.table.get_data()
        return settings


class CategoryVisibility(ChainAction):

    name = 'Category Visibility'

    def apply_category_operations(self, category_operations):
        db = self.plugin_action.gui.current_db
        tag_browser = self.plugin_action.gui.tags_view
        all_categories = [ category.category_key for category \
                    in tag_browser._model.category_nodes if category.category_key.find('.') == -1 ]
        hidden_categories = tag_browser.hidden_categories
        for category_operation in category_operations:
            category_key = category_operation['category_key']
            operation = category_operation['operation']
            if category_key not in all_categories:
                continue
            if numeric_version < (5,14,0):
                is_category_hidden = category_key in hidden_categories
                if operation == 'toggle':
                    if is_category_hidden:
                        operation = 'show'
                    else:
                        operation = 'hide'
                tag_browser.context_menu_handler(action=operation, category=category_key)
            else:
                gui.tb_category_visibility(category_key, operation)

    def run(self, gui, settings, chain):
        category_operations = settings.get('categories_config', [])
        self.apply_category_operations(category_operations)

    def validate(self, settings):
        try:
            if not settings.get('categories_config'):
                raise Exception('Empty settings')
        except:
            return (_('Settings Error'), _('You must configure this action before running it'))
        for category_config in settings.get('categories_config'):
            category_key = category_config.get('category_key')
            operation = category_config.get('operation')
            if not (category_key and operation):
                   return (_('Missing values'), _('Empty values not allowed for name or action'))  
        gui = self.plugin_action.gui
        categories_config = settings['categories_config']
        available_category_keys = set([category.category_key for category in gui.tags_view._model.category_nodes])
        saved_category_keys = set([x['category_key'] for x in categories_config])
        missing_keys = saved_category_keys.difference(available_category_keys)
        if missing_keys:
            return (_('Missing Category(s)'), _('Catetory(s) ({}) not available in current library'.format(missing_keys)))
        return True

    def config_widget(self):
        return ConfigWidget

Last edited by capink; 03-23-2021 at 01:34 PM. Reason: Updated to include latest changes introduced by chaley
capink is offline   Reply With Quote