|
|
#406 | |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Quote:
I've played with gui.tags_view.context_menu_handler(action='hide', category='#genre') and it seems to working. Is there something that makes calling this non desirable? |
|
|
|
|
|
|
#407 | |
|
Grand Sorcerer
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 12,526
Karma: 8065948
Join Date: Jan 2010
Location: Notts, England
Device: Kobo Libra 2
|
Quote:
If you want to play with it then this might work. It uses "recount()' which is what calibre uses when the tag browser data has changed. Code:
tag_browser = gui.tags_view
cats = tag_browser.hidden_categories
# modify cats as needed, adding or removing categories. It is a set()
self.db.new_api.set_pref('tag_browser_hidden_categories', list(cats))
tag_browser.recount()
Code:
change_hidden_categories(operation, category_set) Last edited by chaley; 03-18-2021 at 11:02 AM. |
|
|
|
|
|
|
#408 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
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 02:34 PM. Reason: Updated to include latest changes introduced by chaley |
|
|
|
|
|
#409 | |
|
Grand Sorcerer
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 12,526
Karma: 8065948
Join Date: Jan 2010
Location: Notts, England
Device: Kobo Libra 2
|
Quote:
Add this at line 438 of gui2.tag_browser.ui Code:
def change_tb_category_visibility(self, category, operation):
'''
Hide or show categories in the tag browser. 'category' is the lookup key
to show or hide. Set operation == 'show' or 'hide' as needed.
'''
if category not in self.tags_view.model().categories:
raise ValueError(_('change_tb_category_visibility: category %s does not exist') % category)
cats = self.tags_view.hidden_categories
if operation == 'hide':
cats.add(category)
elif operation == 'show':
cats.discard(category)
else:
raise ValueError(_('change_tb_category_visibility: invalid operation %s') % operation)
self.library_view.model().db.new_api.set_pref('tag_browser_hidden_categories', list(cats))
self.tags_view.recount()
Code:
#tag_browser.context_menu_handler(action=visibility, category=category_key)
# if visibility == 'show':
# hidden_categories.discard(category_key)
# else:
# hidden_categories.add(category_key)
# db.new_api.set_pref('tag_browser_hidden_categories', list(hidden_categories))
# tag_browser.recount()
gui.change_tb_category_visibility(category_key, visibility)
Code:
gui.change_tb_category_visibility(category_key, visibility) |
|
|
|
|
|
|
#410 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
I tested it and it is working.
Thank you.
|
|
|
|
|
|
#411 |
|
Grand Sorcerer
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 12,526
Karma: 8065948
Join Date: Jan 2010
Location: Notts, England
Device: Kobo Libra 2
|
|
|
|
|
|
|
#412 |
|
Grand Sorcerer
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 12,526
Karma: 8065948
Join Date: Jan 2010
Location: Notts, England
Device: Kobo Libra 2
|
The API method is now in source, with a new name and more functionality. Details are:
Code:
def tb_category_visibility(self, category, operation):
'''
Hide or show categories in the tag browser. 'category' is the lookup key.
Operation can be:
- 'show' to show the category in the tag browser
- 'hide' to hide the category
- 'toggle' to invert its visibility
- 'is_visible' returns True if the category is currently visible, False otherwise
'''
|
|
|
|
|
|
#413 |
|
Custom User Title
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 11,369
Karma: 79528341
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
|
Thank you!
|
|
|
|
|
|
#414 | |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Quote:
|
|
|
|
|
|
|
#415 |
|
Custom User Title
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 11,369
Karma: 79528341
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
|
|
|
|
|
|
|
#416 |
|
Custom User Title
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 11,369
Karma: 79528341
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
|
This might've been asked before, but is there a way in Action Chains to "add file to selected book records" with a specified filepath? I keep a dummy.paperbook file in a folder for, well, paper books.
This is a bit of a corner case though and probably not practical to add something specifically for it. |
|
|
|
|
|
#417 | |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Quote:
Code:
from __future__ import (unicode_literals, division, absolute_import,
print_function)
import subprocess
import os
from PyQt5.Qt import (QApplication, Qt, QWidget, QHBoxLayout, QVBoxLayout,
QGroupBox, QToolButton)
from calibre import prints
from calibre.constants import DEBUG, iswindows
from calibre.gui2 import error_dialog, choose_files
from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.common_utils import DragDropComboBox, get_icon
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.template = ''
self._init_controls()
def _init_controls(self):
l = QVBoxLayout()
self.setLayout(l)
self.format_box = QGroupBox(_('&Choose format:'))
l.addWidget(self.format_box)
format_layout = QVBoxLayout()
self.format_box.setLayout(format_layout)
self.format_combo = DragDropComboBox(self, drop_mode='file')
format_layout.addWidget(self.format_combo)
hl1 = QHBoxLayout()
format_layout.addLayout(hl1)
hl1.addWidget(self.format_combo, 1)
self.choose_format_button = QToolButton(self)
self.choose_format_button.setToolTip(_('Choose format'))
self.choose_format_button.setIcon(get_icon('document_open.png'))
self.choose_format_button.clicked.connect(self._choose_file)
hl1.addWidget(self.choose_format_button)
def _choose_file(self):
files = choose_files(None, _('Select format dialog'), _('Select a format'),
all_files=True, select_only_single_file=True)
if not files:
return
format_path = files[0]
if iswindows:
format_path = os.path.normpath(format_path)
self.block_events = True
existing_index = self.format_combo.findText(format_path, Qt.MatchExactly)
if existing_index >= 0:
self.format_combo.setCurrentIndex(existing_index)
else:
self.format_combo.insertItem(0, format_path)
self.format_combo.setCurrentIndex(0)
self.block_events = False
def load_settings(self, settings):
if settings:
self.format_combo.setCurrentText(settings['path_to_format'])
def save_settings(self):
settings = {}
settings['path_to_format'] = self.format_combo.currentText().strip()
return settings
class AddFormatAction(ChainAction):
name = 'Add Format'
support_scopes = True
def run(self, gui, settings, chain):
book_ids = chain.scope().get_book_ids()
db = gui.current_db
path = settings['path_to_format']
try:
fmt = os.path.splitext(path)[-1].lower().replace('.', '').upper()
except:
fmt = ''
for book_id in book_ids:
db.new_api.add_format(book_id, fmt, path)
def validate(self, settings):
if not settings:
return (_('Settings Error'), _('You must configure this action before running it'))
if not settings['path_to_format']:
return (_('No Format'), _('You must specify a path to valid format'))
return True
def config_widget(self):
return ConfigWidget
Last edited by capink; 07-16-2021 at 04:44 AM. |
|
|
|
|
|
|
#418 |
|
Grand Sorcerer
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 12,526
Karma: 8065948
Join Date: Jan 2010
Location: Notts, England
Device: Kobo Libra 2
|
@capink: the changes to the template dialog broke action chains. Opening the template editor now silently dies.
The problem is in these lines of code at (I think) line 431 in templates.py: Code:
try:
self.gridLayout.setRowStretch(self.getWidgetRow(self.textbox), 2)
self.gridLayout.setRowStretch(self.getWidgetRow(self.source_code), 1)
except:
if DEBUG:
prints('Action chains: TemplateBox: Not able to modify stretch')
Code:
self.gridLayout.setRowStretch(self.getWidgetRow(self.source_code), 1) I don't know why these lines are there. While making the changes to add more result lines and ability to change the font, I cleaned up the layout of the dialog. In particular, the function reference stuff is now in its own unnamed grid layout put into a cell in the parent layout. My recommendation is that unless there is a compelling reason you should remove the try/except block changing the stretch factor. You shouldn't need it. If you do then I can look at why and see if it is fixable in the TemplateDialog class itself. |
|
|
|
|
|
#419 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
They are unnecessary. I was experimenting with modifying stretch values and left them there. I will remove them before the next version of calibre is released.
|
|
|
|
|
|
#420 |
|
Custom User Title
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 11,369
Karma: 79528341
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
|
Question: Some of my chains have Kobo Utilities actions that disappear when device is not plugged in, so they appear as red errors in the editor. Is it safe to modify that chain without plugging in the device?
|
|
|
|
![]() |
|
Similar Threads
|
||||
| Thread | Thread Starter | Forum | Replies | Last Post |
| Action Chains Resources | capink | Plugins | 80 | 09-18-2025 05:45 AM |
| [Editor Plugin] Editor Chains | capink | Plugins | 106 | 06-17-2025 06:36 PM |
| [GUI Plugin] Noosfere_util, a companion plugin to noosfere DB | lrpirlet | Plugins | 2 | 08-18-2022 04:15 PM |
| [GUI Plugin] Save Virtual Libraries To Column (GUI) | chaley | Plugins | 14 | 04-04-2021 06:25 AM |