diff --git a/src/calibre/gui2/actions/sort.py b/src/calibre/gui2/actions/sort.py index 882ccbdc8b..c3e75ed1f1 100644 --- a/src/calibre/gui2/actions/sort.py +++ b/src/calibre/gui2/actions/sort.py @@ -7,10 +7,10 @@ __copyright__ = '2013, Kovid Goyal ' from contextlib import suppress from functools import partial -from qt.core import QAction, QIcon, QToolButton, pyqtSignal +from qt.core import QAction, QDialog, QIcon, QToolButton, pyqtSignal from calibre.gui2.actions import InterfaceAction -from calibre.utils.icu import sort_key +from calibre.utils.icu import primary_sort_key from polyglot.builtins import iteritems SORT_HIDDEN_PREF = 'sort-action-hidden-fields' @@ -98,8 +98,9 @@ class SortByAction(InterfaceAction): name_map = {v:k for k, v in iteritems(fm.ui_sortable_field_keys())} hidden = frozenset(db.new_api.pref(SORT_HIDDEN_PREF, default=()) or ()) hidden_items_menu = menu.addMenu(_('Select sortable columns')) + menu.addAction(_('Sort on multiple columns'), self.choose_multisort) menu.addSeparator() - all_names = sorted(name_map, key=sort_key) + all_names = sorted(name_map, key=primary_sort_key) for name in all_names: key = name_map[name] ac = hidden_items_menu.addAction(name) @@ -124,6 +125,12 @@ class SortByAction(InterfaceAction): sac.sort_requested.connect(self.sort_requested) menu.addAction(sac) + def choose_multisort(self): + from calibre.gui2.dialogs.multisort import ChooseMultiSort + d = ChooseMultiSort(self.gui.current_db, parent=self.gui, is_device_connected=self.gui.device_connected) + if d.exec_() == QDialog.DialogCode.Accepted: + self.gui.library_view.multisort(d.current_sort_spec) + def sort_requested(self, key, ascending): if ascending is None: self.gui.library_view.intelligent_sort(key, True) diff --git a/src/calibre/gui2/dialogs/multisort.py b/src/calibre/gui2/dialogs/multisort.py new file mode 100644 index 0000000000..8c5552ec0e --- /dev/null +++ b/src/calibre/gui2/dialogs/multisort.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2021, Kovid Goyal + + +from qt.core import ( + QAbstractItemView, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QSize, Qt, + QVBoxLayout +) + +from calibre import prepare_string_for_xml +from calibre.gui2 import error_dialog +from calibre.gui2.actions.sort import SORT_HIDDEN_PREF +from calibre.gui2.widgets2 import Dialog +from calibre.utils.icu import primary_sort_key + +ascending_symbol = '⏷' +descending_symbol = '⏶' + + +class ChooseMultiSort(Dialog): + + def __init__(self, db, is_device_connected=False, parent=None, hidden_pref=SORT_HIDDEN_PREF): + self.db = db.new_api + self.hidden_fields = set(self.db.pref(SORT_HIDDEN_PREF, default=()) or ()) + if not is_device_connected: + self.hidden_fields.add('ondevice') + fm = self.db.field_metadata + self.key_map = fm.ui_sortable_field_keys().copy() + self.name_map = {v:k for k, v in self.key_map.items()} + self.all_names = sorted(self.name_map, key=primary_sort_key) + self.sort_order_map = dict.fromkeys(self.key_map, True) + super().__init__(_('Sort by multiple columns'), 'multisort-chooser', parent=parent) + + def sizeHint(self): + return QSize(600, 400) + + def setup_ui(self): + self.vl = vl = QVBoxLayout(self) + self.hl = hl = QHBoxLayout() + self.la = la = QLabel(_( + 'Pick multiple columns to sort by. Drag and drop to re-arrange. Higher columns are more important.' + ' Ascending or descending order can be toggled by clicking the column name at the bottom' + ' of this dialog, after having selected it.')) + la.setWordWrap(True) + vl.addWidget(la) + vl.addLayout(hl) + self.order_label = la = QLabel('') + la.setTextFormat(Qt.TextFormat.RichText) + la.setWordWrap(True) + la.linkActivated.connect(self.link_activated) + vl.addWidget(la) + vl.addWidget(self.bb) + + self.column_list = cl = QListWidget(self) + hl.addWidget(cl) + for name in self.all_names: + i = QListWidgetItem(cl) + i.setText(name) + i.setData(Qt.ItemDataRole.UserRole, self.name_map[name]) + cl.addItem(i) + i.setCheckState(Qt.CheckState.Unchecked) + if self.name_map[name] in self.hidden_fields: + i.setHidden(True) + cl.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + cl.currentRowChanged.connect(self.current_changed) + cl.itemDoubleClicked.connect(self.item_double_clicked) + cl.setCurrentRow(0) + cl.itemChanged.connect(self.update_order_label) + cl.model().rowsMoved.connect(self.update_order_label) + + def item_double_clicked(self, item): + cs = item.checkState() + item.setCheckState(Qt.CheckState.Checked if cs == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked) + + def current_changed(self): + self.update_order_label() + + @property + def current_sort_spec(self): + ans = [] + cl = self.column_list + for item in (cl.item(r) for r in range(cl.count())): + if item.checkState() == Qt.CheckState.Checked: + k = item.data(Qt.ItemDataRole.UserRole) + ans.append((k, self.sort_order_map[k])) + return ans + + def update_order_label(self): + t = '' + for i, (k, ascending) in enumerate(self.current_sort_spec): + name = self.key_map[k] + symbol = ascending_symbol if ascending else descending_symbol + if i != 0: + t += ' :: ' + q = bytes.hex(k.encode('utf-8')) + dname = prepare_string_for_xml(name).replace(" ", " ") + t += f' {dname} {symbol}' + if t: + t = _('Effective sort') + ': ' + t + self.order_label.setText(t) + + def link_activated(self, url): + key = bytes.fromhex(url).decode('utf-8') + self.sort_order_map[key] ^= True + self.update_order_label() + + def accept(self): + if not self.current_sort_spec: + return error_dialog(self, _('No sort selected'), _( + 'You must select at least one column on which to sort'), show=True) + super().accept() + + +if __name__ == '__main__': + from calibre.gui2 import Application + app = Application([]) + from calibre.library import db + d = ChooseMultiSort(db()) + d.exec_() + print(d.current_sort_spec) + del d + del app