View Single Post
Old 09-18-2025, 05:45 AM   #81
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,221
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
Here is a custom action that addresses filtering problem, without the need for the user to use template language at all. It allows the following:

Code:
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~

__license__ = 'GPL v3'
__copyright__ = '2025, Ahmed Zaki <azaki00.dev@gmail.com>'
__docformat__ = 'restructuredtext en'

from qt.core import (QWidget, QVBoxLayout, QGroupBox, QRadioButton,
                     QLineEdit, QCheckBox, QLabel)

from calibre import prints
from calibre.constants import DEBUG
from calibre.utils.search_query_parser import ParseException

from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.database import is_search_valid

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):
        l = self.l = QVBoxLayout()
        self.setLayout(l)


        search_lbl = QLabel(_('Calibre search'))
        search_lbl.setToolTip(_('Use a regular calibre search'))
        l.addWidget(search_lbl)
        searchbox = self.searchbox = QLineEdit(self)
        searchbox.textChanged.connect(self._on_searchbox_text_change)
        l.addWidget(searchbox)

        selection_groupbox = QGroupBox(_('Selection options'))
        selection_groupbox_layout = QVBoxLayout()
        selection_groupbox.setLayout(selection_groupbox_layout)
        selected_opt = self.selected_opt = QRadioButton(_('Restrict to currently selected books'))
        selection_groupbox_layout.addWidget(selected_opt)
        selected_opt.setChecked(True)
        all_opt = self.all_opt = QRadioButton(_('Act on all books'))
        selection_groupbox_layout.addWidget(all_opt)
        scope_opt = self.scope_opt = QRadioButton(_('Restrict to books in current scope'))
        selection_groupbox_layout.addWidget(scope_opt)
        saved_selection_opt = self.saved_selection_opt = QRadioButton(_('Restrict to books saved in a variable'))
        selection_groupbox_layout.addWidget(saved_selection_opt)
        retrieve_variable_ledit = self.retrieve_variable_ledit = QLineEdit()
        selection_groupbox_layout.addWidget(retrieve_variable_ledit)
        self.l.addWidget(selection_groupbox)

        save_selection_gb = self.save_selection_gb = QGroupBox(_('Save currently selected books into a variable'))
        save_selection_l = QVBoxLayout()
        save_selection_gb.setLayout(save_selection_l)
        save_selection_gb.setCheckable(True)
        save_selection_lbl = QLabel(_('Variable name'))
        save_selection_ledit = self.save_selection_ledit = QLineEdit()
        save_selection_l.addWidget(save_selection_lbl)
        save_selection_l.addWidget(save_selection_ledit)
        l.addWidget(save_selection_gb)

        self.l.addStretch(1)
        self.setMinimumSize(300,400)

    def _on_searchbox_text_change(self):
        text = self.searchbox.text()
        if text:
            ok = is_search_valid(self.db, text)
            ss = 'QLineEdit { border: 2px solid %s; border-radius: 3px }' % (
            '#50c878' if ok else '#FF2400')
            self.searchbox.setStyleSheet(ss)
        else:
            self.searchbox.setStyleSheet('')

    def load_settings(self, settings):
        if settings:
            search_text = settings.get('search_text', '')
            self.searchbox.setText(search_text)
            superset = settings.get('opt', 'selected_books')
            if superset == 'selected_books':
                self.selected_opt.setChecked(True)
            elif superset == 'current_scope':
                self.scope_opt.setChecked(True)
            elif superset == 'all_books':
                self.all_opt.setChecked(True)
            elif superset == 'saved_selection':
                self.saved_selection_opt.setChecked(True)
                self.retrieve_variable_ledit.setText(settings['retrieve_variable'])
            self.save_selection_gb.setChecked(settings['save_selection'])
            self.save_selection_ledit.setText(settings.get('save_variable', ''))

    def save_settings(self):
        settings = {}
        settings['search_text'] = self.searchbox.text()
        if self.selected_opt.isChecked():
            settings['opt'] = 'selected_books'
        elif self.scope_opt.isChecked():
            settings['opt'] = 'current_scope'
        elif self.all_opt.isChecked():
            settings['opt'] = 'all_books'
        elif self.saved_selection_opt.isChecked():
            settings['opt'] = 'saved_selection'
            settings['retrieve_variable'] = self.retrieve_variable_ledit.text()
        settings['save_selection'] = self.save_selection_gb.isChecked()
        if settings['save_selection']:
            settings['save_variable'] = self.save_selection_ledit.text().strip()
        return settings

class SubsetSelector(ChainAction):

    name = 'Subset Selector'
    _is_builtin = False
    support_scopes = True

    def run(self, gui, settings, chain):
        db = gui.current_db
        cache = {}
        # dont save them yet, cache and save later
        if settings['save_selection']:
            cache['current_ids'] = gui.current_view().get_selected_ids()
        superset = settings.get('opt', 'selected_books')
        search_text = settings['search_text'].strip()
        if search_text:
            if superset == 'selected_books':
                if cache.get('current_ids'):
                    superset_ids = cache['current_ids']
                else:
                    superset_ids = gui.current_view().get_selected_ids()
            elif superset == 'current_scope':
                superset_ids = chain.scope().get_book_ids()
            elif superset == 'all_books':
                superset_ids = gui.current_db.all_ids()
            elif superset == 'saved_selection':
                superset_ids = getattr(chain, 'python_vars', {}).get(settings['retrieve_variable'], None)
                if superset_ids == None:
                    prints(f'Action Chains: Subset Selector: {settings["retrieve_variable"]} is empty')
                    superset_ids = []

            try:
                if not search_text:
                    book_ids = superset_ids
                else:
                    search_ids = db.data.search_getting_ids(search_text, '', use_virtual_library=False)
                    if superset == 'all_books':
                        book_ids = search_ids
                    else:
                        book_ids = list(set(search_ids).intersection(superset_ids))
            except ParseException:
                book_ids = []

            gui.current_view().select_rows(book_ids, change_current=True, scroll=False)

        # Avoid saving selected books early in case the opt
        # saved_selection is ticked
        if settings['save_selection']:
            save_variable = settings['save_variable']
            if not hasattr(chain, 'python_vars'):
                chain.python_vars = {}
            chain.python_vars[save_variable] = cache['current_ids']
            chain.chain_vars[save_variable] = ','.join([str(x) for x in chain.python_vars[save_variable]])


    def validate(self, settings):
        gui = self.plugin_action.gui
        db = gui.current_db
        if not settings:
            return (_('Settings Error'), _('You must configure this superset before running it'))
        search_text = settings['search_text']
        if not is_search_valid(db, search_text):
            return (_('Invalid search'), _(f'Search "{search_text}" is not valid'))
        if settings['save_selection'] and not settings.get('save_variable', '').strip():
            return (_('Empty variable'), _('You must choose a variable name to save books'))
        if settings['opt'] == 'saved_selection' and not settings.get('retrieve_variable', '').strip():
            return (_('Empty variable'), _('You must enter saved variable name'))
        return True

    def config_widget(self):
        return ConfigWidget
  • The user can use the action to save current selections to a named variable (e.g. chain_selected_ids, previously_selected_ids, third_action_ids .... etc)
  • The user can restore the selections from a named variable.
  • The user can apply a regular calibre search that will limit the selection to a subset of one of the following:
    • Currently selected books.
    • Book ids stored in a variable
    • Books in current scope

Notes:
  • Regular calibre search allows for using templates. You consult the calibre search documentation, looking for the section named: "Search using templates"
  • The selection is going to be limited by the current library view including the search vls applied. To clear those, you can use the builtin "Selection Modifier". There is no need to duplicate the same functionality.
  • The action is called "Subset Selector" for lack of better name. More descriptive suggestions are welcome.
  • The action will not be part of the stock plugin until it fully matures and has a stable api.
Attached Thumbnails
Click image for larger version

Name:	1.jpg
Views:	58
Size:	43.0 KB
ID:	218136   Click image for larger version

Name:	2.jpg
Views:	58
Size:	38.1 KB
ID:	218137  

Last edited by capink; 09-18-2025 at 03:42 PM.
capink is offline   Reply With Quote