Register Guidelines E-Books Today's Posts Search

Go Back   MobileRead Forums > E-Book Software > Calibre > Plugins

Notices

Reply
 
Thread Tools Search this Thread
Old 06-16-2025, 01:23 PM   #76
ownedbycats
Custom User Title
ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.
 
ownedbycats's Avatar
 
Posts: 11,915
Karma: 83456789
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
Quote:
Originally Posted by dunhill View Post
I didn't imagine something like this would be planned, and it was making my life complicated. Thanks so much. I'll try it now.
Can't change VLs directly in AC. I'd do a selection modifiier, clear VL and select books based on a template search virtual_libraries() (or a composite column using that). Then view action.
ownedbycats is offline   Reply With Quote
Old 06-16-2025, 01:25 PM   #77
dunhill
Wizard
dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.dunhill ought to be getting tired of karma fortunes by now.
 
dunhill's Avatar
 
Posts: 1,215
Karma: 1513034
Join Date: Sep 2017
Location: Buenos Aires, Argentina
Device: moon+ reader, kindle paperwhite, Linux Mint
Quote:
Originally Posted by theducks View Post
Be prepared to keep being Amazed as to all the things Kovid and his helpers have included in Calibre

IIRC this one has been there since VL's was first added.
It's true that the universe within Calibre is vast.
Okay, Calibre opens from the virtual library, but what I was looking for through action chains was for the book viewer to open books from that virtual library.
dunhill is offline   Reply With Quote
Old 06-16-2025, 01:45 PM   #78
chaley
Grand Sorcerer
chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.chaley ought to be getting tired of karma fortunes by now.
 
Posts: 12,529
Karma: 8125420
Join Date: Jan 2010
Location: Notts, England
Device: Kobo Libra 2
Quote:
Originally Posted by ownedbycats View Post
Can't change VLs directly in AC. I'd do a selection modifiier, clear VL and select books based on a template search virtual_libraries() (or a composite column using that). Then view action.
No need for a template search. In the selection modifier, clear all VLs etc, search "vl:the_vl_name and formats:true", then select all books in view. The formats:true is there to filter out any books that don't have a format.

The attached chain shows how to do it. Change the name of the VL in the selection modifier to what you want.
Attached Files
File Type: zip View books in virtual library.zip (518 Bytes, 123 views)
chaley is offline   Reply With Quote
Old 08-05-2025, 05:01 AM   #79
un_pogaz
Chalut o/
un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.
 
un_pogaz's Avatar
 
Posts: 726
Karma: 741392
Join Date: Dec 2017
Device: Kobo
Re-calc Book Size

A little module to re-calc the size column of a book.
Useful if you've had edited the book files by a third-party program.

EDIT: If your have Quality Check 1.14.3 and above installed, you can use directly the same action in "Imported From Other Plugin > Quality Check > Run Quality Check fix" and select "Check and repair book size"

Code:
import os
from collections import defaultdict

from calibre_plugins.action_chains.actions.base import ChainAction

class ReCalcSize(ChainAction):
    
    name = 'Re-calc Size'
    
    def run(self, gui, settings, chain):
        db = gui.current_db.new_api
        ids = chain.scope().get_book_ids()
        updated = defaultdict(set)
        
        for book_id in ids:
            for fmt in db.formats(book_id):
                path = db.format_abspath(book_id, fmt)
                if path and os.path.exists(path):
                    size_db = db.format_db_size(book_id, fmt)
                    size_new = os.path.getsize(path)
                    if size_db != size_new:
                        db.format_metadata(book_id, fmt, update_db=True)
                        updated[book_id].add(fmt)
        
        db.update_last_modified(updated.keys())
        
        msg = f'Size updated for {len(updated)} books'
        gui.status_bar.showMessage(msg)
Attached Files
File Type: py Re-calc Size.py (966 Bytes, 42 views)

Last edited by un_pogaz; 05-07-2026 at 05:07 AM.
un_pogaz is offline   Reply With Quote
Old 09-17-2025, 04:22 PM   #80
ownedbycats
Custom User Title
ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.
 
ownedbycats's Avatar
 
Posts: 11,915
Karma: 83456789
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
Quote:
Originally Posted by ownedbycats View Post
For FanFicFare users, this chain I made to easily add a new StoryURL section. First it copies this to clipboard:

Code:
## Test Author - Test Title
[https://www.fanfiction.net/s/123456/1/]

Then it opens personal.ini to paste it in.
Here's my enhanced version of the chain that additionally also copies story-specific settings from a column. This allows me to reduce the size of personal.ini by removing settings for completed stories while still keeping them available if I later need them.

to use, create a long-text column (interpret type: plain text) with lookup name #fanficsettings.

As I suggest editing settings in the personal.ini editor rather than the column itself, I also included Calibre ID in the commented header to make it easier to locate the book the section's associated with. An example from my library:

Code:
## stillwaters01 - There Were Days (8382)
[https://archiveofourown.org/works/959950]
exclude_notes:chaptersummary
Attached Files
File Type: zip CopyStoryUrl.zip (2.5 KB, 108 views)
ownedbycats is offline   Reply With Quote
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,223
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:	151
Size:	43.0 KB
ID:	218136   Click image for larger version

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

Last edited by capink; 09-18-2025 at 03:42 PM.
capink is offline   Reply With Quote
Old 03-25-2026, 09:44 AM   #82
criscros
Fulltime reader
criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.criscros can eat soup with a fork.
 
Posts: 53
Karma: 9296
Join Date: Mar 2021
Device: Kobo Clara
I download a few webnovels that I load on my Libra2. After sending an update to the reader, I have a simple action chain that updates the TOC and then restores the bookmark, both are just calibre actions.

When I run the chain, I have to click to affirm 4 times for it to finish. Is there a way to automate the affirm actions?

I have copied the code for a few actions here with no issues, but writing it is not something I understand enough to do.

Is what I'm asking possible?
criscros is offline   Reply With Quote
Old 05-07-2026, 02:51 AM   #83
un_pogaz
Chalut o/
un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.
 
un_pogaz's Avatar
 
Posts: 726
Karma: 741392
Join Date: Dec 2017
Device: Kobo
Re-calc Pages recount (bulk)

Here a little module to call a pages recount on several books without to call it on the entier library for the buildin Pages column.

Note that this module does not execute directly the re-calc, it only add the targeted books to the pending queue, then Calibre will scan the books at it own pace in the background.

Also the module call for a safe recount, therefore is strongly recommended that you execute a book size re-calc action before execute this module.

Calibre version 9 and above only.

Code:
from calibre_plugins.action_chains.actions.base import ChainAction

class ReCalcPages(ChainAction):
    
    name = 'Re-calc Pages'
    
    def run(self, gui, settings, chain):
        db = gui.current_db.new_api
        ids = chain.scope().get_book_ids()
        
        msg = f'Adding {len(ids)} books to the Pages recount queue...'
        gui.status_bar.showMessage(msg)
        
        ids = tuple((x,) for x in ids)
        with db.write_lock:
            db.backend.executemany('UPDATE books_pages_link SET needs_scan=1 WHERE book=?', ids)
        db.queue_pages_scan()
        
        msg = f'{len(ids)} books added to the Pages recount queue'
        gui.status_bar.showMessage(msg)
Attached Files
File Type: py Re-calc Pages.py (696 Bytes, 45 views)

Last edited by un_pogaz; 05-07-2026 at 03:19 AM.
un_pogaz is offline   Reply With Quote
Old 05-07-2026, 03:16 AM   #84
un_pogaz
Chalut o/
un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.un_pogaz ought to be getting tired of karma fortunes by now.
 
un_pogaz's Avatar
 
Posts: 726
Karma: 741392
Join Date: Dec 2017
Device: Kobo
Also, the Re-calc Book Size module it has been updated to a version that correctly update all format sizes.
un_pogaz is offline   Reply With Quote
Old 10-29-2026, 05:55 AM   #85
ownedbycats
Custom User Title
ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.ownedbycats ought to be getting tired of karma fortunes by now.
 
ownedbycats's Avatar
 
Posts: 11,915
Karma: 83456789
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
Quote:
Originally Posted by capink View Post
This action allows the user to assign View Manager views to virtual libraries. When used with "VL Tab Changed" event, it will automatically apply the corresponding view whenever the user switches VL tabs.

Code:
import copy
from functools import partial

from qt.core import (QApplication, Qt, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
                     QGroupBox, QAbstractTableModel, QModelIndex, QSizePolicy,
                     QToolButton, QSpacerItem, QIcon, QBrush, pyqtSignal)

from calibre import prints
from calibre.constants import DEBUG
from calibre.gui2 import error_dialog
from calibre.gui2.widgets2 import Dialog

from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.common_utils import get_icon, ViewLog
from calibre_plugins.action_chains.database import get_valid_vls
from calibre_plugins.action_chains.gui.delegates import ComboDelegate
from calibre_plugins.action_chains.gui.models import UP, DOWN
from calibre_plugins.action_chains.gui.views import TableView
import calibre_plugins.action_chains.config as cfg

ALL_BOOKS = '_ALL_BOOKS'
KEY_TABS_VIEWS_TABLE_STATE = 'tabsViewsTableStates'

def get_vls(db):
    vls = get_valid_vls(db)
    vls.insert(0, ALL_BOOKS)
    return vls

def view_manager_views(gui):
    try:
        import calibre_plugins.view_manager.config as vm_cfg
        views = vm_cfg.get_library_config(gui.current_db)[vm_cfg.KEY_VIEWS]
        return views
    except:
        import traceback
        print(traceback.format_exc())
        return []

class TabsModel(QAbstractTableModel):

    error = pyqtSignal(str, str)

    def __init__(self, plugin_action, tabs_config=[]):
        QAbstractTableModel.__init__(self)
        self.tabs_config = tabs_config
        self.plugin_action = plugin_action
        self.gui = self.plugin_action.gui
        self.db = self.gui.current_db
        self.col_map = ['tab_name','view_name','errors']
        self.editable_columns = ['tab_name','view_name']
        #self.hidden_cols = ['errors']
        self.hidden_cols = []
        self.col_min_width = {
            'tab_name': 300,
            'view_name': 300
        }
        all_headers = [_('VL'), _('View'), _('errors')]
        self.headers = all_headers

    def rowCount(self, parent):
        if parent and parent.isValid():
            return 0
        return len(self.tabs_config)

    def columnCount(self, parent):
        if parent and parent.isValid():
            return 0
        return len(self.headers)

    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            return self.headers[section]
        elif role == Qt.DisplayRole and orientation == Qt.Vertical:
            return section + 1
        return None

    def data(self, index, role):
        if not index.isValid():
            return None;
        row, col = index.row(), index.column()
        if row < 0 or row >= len(self.tabs_config):
            return None
        tab_config = self.tabs_config[row]
        col_name = self.col_map[col]
        value = tab_config.get(col_name, '')
        error = tab_config.get('errors', '')

        if role in [Qt.DisplayRole, Qt.UserRole, Qt.EditRole]:
            if col_name == 'errors':
                if error:
                    return error
            else:
                return value

        elif role == Qt.DecorationRole:
            if col_name == 'errors':
                if error:
                    return QIcon(get_icon('dialog_error.png'))
                
        elif role == Qt.ToolTipRole:
            if col_name == 'errors':
                if error:
                    return error

        elif role == Qt.ForegroundRole:
            color = None
            if error:
                color = Qt.red
            if color is not None:
                return QBrush(color)

        return None

    def setData(self, index, value, role):
        done = False

        row, col = index.row(), index.column()
        tab_config = self.tabs_config[row]
        val = str(value).strip()
        col_name = self.col_map[col]
        
        if role == Qt.EditRole:
            # make sure no duplicate event entries
            if col_name == 'tab_name':
                old_name = self.data(index, Qt.DisplayRole)
                names = self.get_names()
                if old_name in names:
                    names.remove(old_name)
                if val in names:
                    msg = _('Duplicate vls')
                    details = _('Name ({}) is used in more than one entry'.format(val))
                    self.error.emit(msg, details)
                else:
                    tab_config[col_name] = val
            else:
                tab_config[col_name] = val
            done = True
            
        return done

    def flags(self, index):
        flags = QAbstractTableModel.flags(self, index)
        if index.isValid():
            tab_config = self.tabs_config[index.row()]
            col_name = self.col_map[index.column()]
            if col_name in self.editable_columns:
                flags |= Qt.ItemIsEditable
        return flags

    def insertRows(self, row, count, idx):
        self.beginInsertRows(QModelIndex(), row, row + count - 1)
        for i in range(0, count):
            tab_config = {}
            tab_config['tab_name'] = ''
            tab_config['view_name'] = ''
            self.tabs_config.insert(row + i, tab_config)
        self.endInsertRows()
        return True

    def removeRows(self, row, count, idx):
        self.beginRemoveRows(QModelIndex(), row, row + count - 1)
        for i in range(0, count):
            self.tabs_config.pop(row + i)
        self.endRemoveRows()
        return True

    def move_rows(self, rows, direction=DOWN):
        srows = sorted(rows, reverse=direction == DOWN)
        for row in srows:
            pop = self.tabs_config.pop(row)
            self.tabs_config.insert(row+direction, pop)
        self.layoutChanged.emit()

    def get_names(self):
        names = []
        col = self.col_map.index('tab_name')
        for row in range(self.rowCount(QModelIndex())):
            index = self.index(row, col, QModelIndex())
            name = self.data(index, Qt.DisplayRole)
            # empty name belong to separators, dont include
            if name:
                names.append(name)
        return names

    def validate(self):
        for tab_config in self.tabs_config:
            errors = []
            tab_name = tab_config['tab_name']
            view_name = tab_config['view_name']
            if tab_name not in get_vls(self.db):
                errors.append(_('VL is not available'))
            if view_name not in view_manager_views(self.gui):
                errors.append(_('View is not available'))
            if errors:
                tab_config['errors'] = ' ::: '.join(errors)

class TabsTable(TableView):

    def __init__(self, parent):
        TableView.__init__(self, parent)
        self.plugin_action = parent.plugin_action
        self.doubleClicked.connect(self._on_double_clicked)
        self.gui = self.plugin_action.gui
        self.db = self.gui.current_db
        self.horizontalHeader().setStretchLastSection(False)
        #self.setShowGrid(False)

    def set_model(self, _model):
        self.setModel(_model)
        _model.error.connect(lambda *args: error_dialog(self, *args, show=True))
        self.col_map = _model.col_map

        # Hide columns
        for col_name in _model.hidden_cols:
            col = self.col_map.index(col_name)
            self.setColumnHidden(col, True)

        self.tabs_delegate = ComboDelegate(self, get_vls(self.db))
        self.setItemDelegateForColumn(self.col_map.index('tab_name'), self.tabs_delegate)

        self.views_delegate = ComboDelegate(self, view_manager_views(self.gui))
        self.setItemDelegateForColumn(self.col_map.index('view_name'), self.views_delegate)

        self.resizeColumnsToContents()
        # Make sure every other column has a minimum width
        for col_name, width in _model.col_min_width.items():
            col = self.col_map.index(col_name)
            self._set_minimum_column_width(col, width)

    def _on_double_clicked(self, index):
        m = self.model()
        col_name = m.col_map[index.column()]
        if col_name == 'errors':
            tab_config = m.tabs_config[index.row()]
            details = tab_config.get('errors', '')
            self._view_error_details(details)

    def _view_error_details(self, details):
        ViewLog(_('Errors details'), details, self)

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.tabs_config = tabs_config
        self._init_controls()

    def _init_controls(self):
        self.setWindowTitle(_('Configure views'))
        self.l = l = QVBoxLayout()
        self.setLayout(l)

        settings_l = QGridLayout()
        l.addLayout(settings_l)

        _table_gb = QGroupBox(_('Views'))
        _table_l = QHBoxLayout()
        _table_gb.setLayout(_table_l)
        l.addWidget(_table_gb)
        
        self._table = TabsTable(self)
        _table_l.addWidget(self._table)
        
        _model = self._model = TabsModel(self.plugin_action)
        _model.validate()
        self._table.set_model(_model)
        self._table.selectionModel().selectionChanged.connect(self._on_table_selection_change)
        
        # restore table state
        state = cfg.plugin_prefs.get(KEY_TABS_VIEWS_TABLE_STATE, None)
        if state:
            self._table.apply_state(state)

        # Add a vertical layout containing the the buttons to move up/down etc.
        button_layout = QVBoxLayout()
        _table_l.addLayout(button_layout)
        
        move_up_button = self.move_up_button = QToolButton(self)
        move_up_button.setToolTip(_('Move row up'))
        move_up_button.setIcon(QIcon(I('arrow-up.png')))
        button_layout.addWidget(move_up_button)
        spacerItem1 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        button_layout.addItem(spacerItem1)

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

        delete_button = self.delete_button = QToolButton(self)
        delete_button.setToolTip(_('Delete row'))
        delete_button.setIcon(QIcon(I('minus.png')))
        button_layout.addWidget(delete_button)
        spacerItem4 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        button_layout.addItem(spacerItem4)

        move_down_button = self.move_down_button = QToolButton(self)
        move_down_button.setToolTip(_('Move row down'))
        move_down_button.setIcon(QIcon(I('arrow-down.png')))
        button_layout.addWidget(move_down_button)

        move_up_button.clicked.connect(partial(self._table.move_rows,UP))
        move_down_button.clicked.connect(partial(self._table.move_rows,DOWN))
        add_button.clicked.connect(self._table.add_row)
        delete_button.clicked.connect(self._table.delete_rows)
        
        self._on_table_selection_change()

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

    def _on_table_selection_change(self):
        sm = self._table.selectionModel()
        selection_count = len(sm.selectedRows())
        self.delete_button.setEnabled(selection_count > 0)
        self.move_up_button.setEnabled(selection_count > 0)
        self.move_down_button.setEnabled(selection_count > 0)

    def save_table_state(self):
        # save table state
        cfg.plugin_prefs[KEY_TABS_VIEWS_TABLE_STATE] = self._table.get_state()

    def load_settings(self, settings):
        self._model.tabs_config = settings['tabs_config']
        self._model.validate()
        self._model.layoutChanged.emit()

    def save_settings(self):
        self.save_table_state()
        
        settings = {}
        tabs_config = self._table.model().tabs_config
        # remove error keys from event_members
        for tab_config in tabs_config:
            try:
                del tab_config['errors']
            except:
                pass
        settings['tabs_config'] = tabs_config
        return settings


class SwitchToVLView(ChainAction):

    name = 'Switch To VL View'      

    def run(self, gui, settings, chain):
        idx = gui.vl_tabs.currentIndex()
        vl = str(gui.vl_tabs.tabData(idx) or '').strip() or ALL_BOOKS
        print('debug1: vl: {}'.format(vl))
        if vl:
            view = self.vl_view_lookup(vl, settings['tabs_config'])
            print('debug2: view: {}'.format(view))
            if view:
                if not view in view_manager_views(gui):
                    if DEBUG:
                        prints('Action Chains: Switch To VL View: view ({}) is not available'.format(view))
                    return                  
                self.switch_view(gui, view, vl)      
            else:
                if DEBUG:
                    prints('Action Chains: Switch To VL View: VL Tab ({}) has no configured view'.format(vl))

    def vl_view_lookup(self, vl, tabs_config):
        for tab_config in tabs_config:
            if vl == tab_config['tab_name']:
                return tab_config['view_name']

    def switch_view(self, gui, view, vl):
        view_manager = gui.iactions.get('View Manager')
        if view_manager:
            import calibre_plugins.view_manager.config as vm_cfg
            library_config = vm_cfg.get_library_config(gui.current_db)
            view_info = copy.deepcopy(library_config[vm_cfg.KEY_VIEWS][view])
            ####
            view_info[vm_cfg.KEY_APPLY_VIRTLIB] = False
            ####
            selected_ids = gui.library_view.get_selected_ids()
            # Persist this as the last selected view
            if library_config.get(vm_cfg.KEY_LAST_VIEW, None) != view:
                library_config[vm_cfg.KEY_LAST_VIEW] = view
                vm_cfg.set_library_config(gui.current_db, library_config)

#            if view_info.get(vm_cfg.KEY_APPLY_VIRTLIB,False):
#                view_manager.apply_virtlib(view_info[vm_cfg.KEY_VIRTLIB])
            if view_info[vm_cfg.KEY_APPLY_RESTRICTION]:
                view_manager.apply_restriction(view_info[vm_cfg.KEY_RESTRICTION])
            if view_info[vm_cfg.KEY_APPLY_SEARCH]:
                view_manager.apply_search(view_info[vm_cfg.KEY_SEARCH])
            view_manager.apply_column_and_sort(view_info)

            gui.library_view.select_rows(selected_ids)
            view_manager.current_view = view
            view_manager.rebuild_menus()
        else:
            if DEBUG:
                prints('Action Chains: Switch To VL View: View Manager Plugin not available')

    def validate(self, settings):
        if not settings:
            return (_('Settings Error'), _('You must configure this action before running it'))
        return True

    def config_widget(self):
        return ConfigWidget
Recently Action Chains updated adding the option to show/hide various UI elements (cover grid, book details) with a view. It doesn't work if I use this action though. kiwidude confirmed that this code didn't account for it.
ownedbycats is offline   Reply With Quote
Reply


Forum Jump

Similar Threads
Thread Thread Starter Forum Replies Last Post
[GUI Plugin] Action Chains capink Plugins 1597 11-08-2026 06:49 PM
Book Scanning tool chains tomsem Workshop 17 12-03-2023 10:19 AM
Mystery and Crime Thorne, Guy: Chance in Chains (1914); v1 Pulpmeister Kindle Books 0 11-25-2018 10:09 PM
Mystery and Crime Thorne, Guy: Chance in Chains (1914); v1 Pulpmeister ePub Books 0 11-25-2018 10:08 PM
Could this be the last year for the big chains? Connallmac News 66 01-07-2011 05:11 PM


All times are GMT -4. The time now is 10:57 AM.


MobileRead.com is a privately owned, operated and funded community.