Allow creating sorts based on multiple columns (Add the Sort action to the toolbar via Preferences->Toolbars & menus)

Fixes #1945891 [[Enhancement] Select sortable columns in a separate window](https://bugs.launchpad.net/calibre/+bug/1945891)
This commit is contained in:
Kovid Goyal 2021-10-06 14:38:52 +05:30
parent 9249e92c0a
commit ecdc1007c1
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 133 additions and 3 deletions

View File

@ -7,10 +7,10 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
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)

View File

@ -0,0 +1,123 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
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(" ", "&nbsp;")
t += f' <a href="{q}" style="text-decoration: none">{dname}&nbsp;{symbol}</a>'
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