View Single Post
Old 12-21-2021, 05:30 AM   #22
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,092
Karma: 1948136
Join Date: Aug 2015
Device: Kindle
Multisort action

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

Last edited by capink; 01-07-2022 at 06:17 AM.
capink is offline   Reply With Quote