mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
9249e92c0a
commit
ecdc1007c1
@ -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)
|
||||
|
123
src/calibre/gui2/dialogs/multisort.py
Normal file
123
src/calibre/gui2/dialogs/multisort.py
Normal 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(" ", " ")
|
||||
t += f' <a href="{q}" style="text-decoration: none">{dname} {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
|
Loading…
x
Reference in New Issue
Block a user