View Single Post
Old 04-27-2023, 06:27 PM   #45
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: 8,841
Karma: 62032373
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
Quote:
Originally Posted by capink View Post
Code:
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~

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

from qt.core import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout,
                     QLabel, QGroupBox, QToolButton, QPushButton, QScrollArea, QComboBox,
                     QRadioButton, QCheckBox, QSizePolicy)

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

from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.common_utils import get_icon

try:
    load_translations()
except NameError:
    prints("ActionChains::actions/sort.py - exception when loading translations")
    pass


def get_cols(db):
    standard = [
        'title',
        'authors',
        'tags',
        'series',
        'publisher',
        'pubdate',
        'rating',
        'languages',
        'last_modified',
        'timestamp',
        'comments',
        'author_sort',
        'sort',
        'marked',
        'identifiers',
        'cover',
        'formats'
    ]                
    custom = sorted([ k for k,v in db.field_metadata.custom_field_metadata().items() if v['datatype'] not in [None,'composite'] ])
    return standard + custom

class SortControl(QGroupBox):
    
    def __init__(self, plugin_action, possible_cols):
        self.plugin_action = plugin_action
        self.possible_cols = possible_cols
        self.gui = plugin_action.gui
        self.db = self.gui.current_db
        self._init_controls()

    def _init_controls(self):
        QGroupBox.__init__(self)
        
        l = QGridLayout()
        self.setLayout(l)

        row_idx = 0
        remove_label = QLabel('<a href="close">✕</a>')
        remove_label.setToolTip(_('Remove'))
        remove_label.linkActivated.connect(self._remove)
        l.addWidget(remove_label, row_idx, 1, 1, 1, Qt.AlignRight)
        row_idx += 1

        gb1 = QGroupBox('')
        gb1_l = QVBoxLayout()
        gb1.setLayout(gb1_l)

        gb1_text = _('Column:')
        self.col_combo_box = QComboBox()
        self.col_combo_box.addItems(self.possible_cols)
        self.col_combo_box.setCurrentIndex(-1)
        gb1_l.addWidget(self.col_combo_box)
            
        gb1.setTitle(gb1_text)
        l.addWidget(gb1, row_idx, 0, 1, 1)


        gb2 = QGroupBox(_('Sort direction'), self)
        gb2_l = QVBoxLayout()
        gb2.setLayout(gb2_l)

        self.button_ascend = QRadioButton(_('Ascending'), self)
        gb2_l.addWidget(self.button_ascend)
        self.button_ascend.setChecked(True)
        self.button_descend = QRadioButton(_('Descending'), self)
        gb2_l.addWidget(self.button_descend)
        self.button_descend.setChecked(False)

        l.addWidget(gb2, row_idx, 1, 1, 1)
        row_idx += 1

        l.setColumnStretch(0, 1)        
        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)

    def apply_sort_filter(self, sort_filter):
        field, is_ascending = sort_filter
        self.col_combo_box.setCurrentText(field)
        self.button_ascend.setChecked(is_ascending)
        self.button_descend.setChecked(not is_ascending)

    def _remove(self):
        self.setParent(None)
        self.deleteLater()

    def isComplete(self):
        '''returns True only if a field and direction are chosen'''
        if self.col_combo_box.currentText() == '':
            return False
        return True
    
    def get_sort_filter(self):
        field = self.col_combo_box.currentText()
        is_ascending = self.button_ascend.isChecked()
        return (field, is_ascending)

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

    def _init_controls(self):
        QWidget.__init__(self)
        l = QVBoxLayout()
        self.setLayout(l)
        
        hl1 = QHBoxLayout()
        clear_button = QPushButton(_('Clear'))
        clear_button.setToolTip(_('Clear all filters'))
        clear_button.setIcon(get_icon('clear_left.png'))
        clear_button.clicked.connect(self.reset)
        hl1.addWidget(clear_button)
        hl1.addStretch(1)
        hl1.addStretch(1)
        add_button = QPushButton(_('Add Sort Filter'))
        add_button.setToolTip(_('Add a column to sort by'))
        add_button.setIcon(get_icon('plus.png'))
        add_button.clicked.connect(self.add_control)
        hl1.addWidget(add_button)
        
        l.addLayout(hl1)

        w = QWidget(self)
        self.controls_layout = QVBoxLayout()
        self.controls_layout.setSizeConstraint(self.controls_layout.SetMinAndMaxSize)
        w.setLayout(self.controls_layout)
        
        scroll = QScrollArea()
        scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll.setWidgetResizable(True)
        scroll.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        scroll.setObjectName('myscrollarea')
        scroll.setStyleSheet('#myscrollarea {background-color: transparent}')
        scroll.setWidget(w)
         
        l.addWidget(scroll)
        
        self._add_control(sort_filter={})

    def isComplete(self):
        '''return True if all controls have fields and algorithms set'''
        for idx in range(self.controls_layout.count()):
            control = self.controls_layout.itemAt(idx).widget()
            if not control.isComplete():
                return False
        return True

    def _add_control(self, sort_filter=None):
        control = SortControl(self.plugin_action, self.possible_cols)
        if sort_filter:
            control.apply_sort_filter(sort_filter)
        self.controls_layout.addWidget(control)

    def add_control(self):
        if not self.isComplete():
            error_dialog(
                self,
                _('Incomplete Sort Filter'),
                _('You must complete the previous sort filter(s) to proceed.'),
                show=True
            )
            return
        self._add_control()

    def reset(self, add_empty_control=True):
        # remove controls in reverse order
        for idx in reversed(range(self.controls_layout.count())):
            control = self.controls_layout.itemAt(idx).widget()
            control.setParent(None)
            control.deleteLater()
        if add_empty_control:
            self._add_control()

    def get_sort_filters(self):
        all_filters = []
        for idx in range(self.controls_layout.count()):
            control = self.controls_layout.itemAt(idx).widget()
            all_filters.append(control.get_sort_filter())
        return all_filters

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.possible_cols = get_cols(self.db)
        self._init_controls()

    def _init_controls(self):
        l = QVBoxLayout()
        self.setLayout(l)

        self.container = SortControlsContainer(self.plugin_action, self.possible_cols)
        l.addWidget(self.container, 1)
        
        self.resize(500, 600)
        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)

    def load_settings(self, settings):
        sort_filters = settings['sort_filters']

        self.container.reset(add_empty_control=False)

        for sort_filter in sort_filters:
            self.container._add_control(sort_filter)

    def save_settings(self):
        settings = {'sort_filters': self.container.get_sort_filters()}
        return settings

class SortAction(ChainAction):

    name = 'Sort by field'
    #_is_builtin = True

    def run(self, gui, settings, chain):
        sort_filters = settings['sort_filters']

        gui.library_view.multisort(sort_filters)

    def validate(self, settings):
        gui = self.plugin_action.gui
        db = gui.current_db
        if not settings:
            return (_('Settings Error'), _('You must configure this action before running it'))
        for sort_filter in settings['sort_filters']:
            field, is_ascending = sort_filter
            if not field:
                return _('No field'), _('You must specify a field for all filters')
            elif not field in get_cols(db):
                return _('Field unavailabe'), _('Current library does not have a field called {}'.format(field))
        return True

    def config_widget(self):
        return ConfigWidget
Note: After one of the recent Calibre updates, 'id' and 'path' are available as standard columns. Adding them to the 'standard' list worked when I tested.

Question: This line prevents composite columns from showing:

Code:
    custom = sorted([ k for k,v in db.field_metadata.custom_field_metadata().items() if v['datatype'] not in [None,'composite'] ])
Removing ,'composite' shows them. But is there a technical reason to avoid this?

Last edited by ownedbycats; 04-27-2023 at 06:37 PM.
ownedbycats is online now   Reply With Quote