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