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