Tag editor: Use a proxy model for better filter performance

Also simplifies the code
This commit is contained in:
Kovid Goyal 2022-02-06 11:58:36 +05:30
parent cab955630c
commit 6fae77e7f4
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 63 additions and 90 deletions

View File

@ -1,13 +1,16 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from qt.core import Qt, QDialog, QAbstractItemView, QApplication from qt.core import (
QAbstractItemView, QApplication, QDialog, QSortFilterProxyModel,
QStringListModel, Qt
)
from calibre.constants import islinux
from calibre.gui2 import error_dialog, gprefs, question_dialog
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_editor_ui import Ui_TagEditor from calibre.gui2.dialogs.tag_editor_ui import Ui_TagEditor
from calibre.gui2 import question_dialog, error_dialog, gprefs from calibre.utils.icu import sort_key
from calibre.constants import islinux
from calibre.utils.icu import sort_key, primary_contains
class TagEditor(QDialog, Ui_TagEditor): class TagEditor(QDialog, Ui_TagEditor):
@ -62,11 +65,13 @@ class TagEditor(QDialog, Ui_TagEditor):
if tags: if tags:
if not self.is_names: if not self.is_names:
tags.sort(key=sort_key) tags.sort(key=sort_key)
for tag in tags:
self.applied_tags.addItem(tag)
else: else:
tags = [] tags = []
self.applied_model = QStringListModel(tags)
p = QSortFilterProxyModel()
p.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
p.setSourceModel(self.applied_model)
self.applied_tags.setModel(p)
if self.is_names: if self.is_names:
self.applied_tags.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.applied_tags.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.applied_tags.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.applied_tags.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
@ -75,11 +80,12 @@ class TagEditor(QDialog, Ui_TagEditor):
all_tags = [tag for tag in self.db.all_custom(label=key)] all_tags = [tag for tag in self.db.all_custom(label=key)]
else: else:
all_tags = [tag for tag in self.db.all_tags()] all_tags = [tag for tag in self.db.all_tags()]
all_tags = sorted(set(all_tags), key=sort_key) all_tags = sorted(set(all_tags) - set(tags), key=sort_key)
q = set(tags) self.all_tags_model = QStringListModel(all_tags)
for tag in all_tags: p = QSortFilterProxyModel()
if tag not in q: p.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.available_tags.addItem(tag) p.setSourceModel(self.all_tags_model)
self.available_tags.setModel(p)
connect_lambda(self.apply_button.clicked, self, lambda self: self.apply_tags()) connect_lambda(self.apply_button.clicked, self, lambda self: self.apply_tags())
connect_lambda(self.unapply_button.clicked, self, lambda self: self.unapply_tags()) connect_lambda(self.unapply_button.clicked, self, lambda self: self.unapply_tags())
@ -98,10 +104,10 @@ class TagEditor(QDialog, Ui_TagEditor):
getattr(self, gprefs.get('tag_editor_last_filter', 'add_tag_input')).setFocus() getattr(self, gprefs.get('tag_editor_last_filter', 'add_tag_input')).setFocus()
if islinux: if islinux:
self.available_tags.itemDoubleClicked.connect(self.apply_tags) self.available_tags.doubleClicked.connect(self.apply_tags)
else: else:
self.available_tags.itemActivated.connect(self.apply_tags) self.available_tags.activated.connect(self.apply_tags)
self.applied_tags.itemActivated.connect(self.unapply_tags) self.applied_tags.activated.connect(self.unapply_tags)
geom = gprefs.get('tag_editor_geometry', None) geom = gprefs.get('tag_editor_geometry', None)
if geom is not None: if geom is not None:
@ -110,10 +116,11 @@ class TagEditor(QDialog, Ui_TagEditor):
def edit_box_changed(self, which): def edit_box_changed(self, which):
gprefs['tag_editor_last_filter'] = which gprefs['tag_editor_last_filter'] = which
def delete_tags(self, item=None): def delete_tags(self):
confirms, deletes = [], [] confirms, deletes = [], []
items = self.available_tags.selectedItems() if item is None else [item] row_indices = list(self.available_tags.selectionModel().selectedRows())
if not items:
if not row_indices:
error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec() error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec()
return return
if not confirm( if not confirm(
@ -121,93 +128,66 @@ class TagEditor(QDialog, Ui_TagEditor):
'tag_editor_delete'): 'tag_editor_delete'):
return return
pos = self.available_tags.verticalScrollBar().value() pos = self.available_tags.verticalScrollBar().value()
for item in items: for ri in row_indices:
used = self.db.is_tag_used(str(item.text())) \ tag = ri.data()
used = self.db.is_tag_used(tag) \
if self.key is None else \ if self.key is None else \
self.db.is_item_used_in_multiple(str(item.text()), label=self.key) self.db.is_item_used_in_multiple(tag, label=self.key)
if used: if used:
confirms.append(item) confirms.append(ri)
else: else:
deletes.append(item) deletes.append(ri)
if confirms: if confirms:
ct = ', '.join([str(item.text()) for item in confirms]) ct = ', '.join(item.data() for item in confirms)
if question_dialog(self, _('Are your sure?'), if question_dialog(self, _('Are your sure?'),
'<p>'+_('The following tags are used by one or more books. ' '<p>'+_('The following tags are used by one or more books. '
'Are you certain you want to delete them?')+'<br>'+ct): 'Are you certain you want to delete them?')+'<br>'+ct):
deletes += confirms deletes += confirms
for item in deletes: for item in sorted(deletes, key=lambda r: r.row(), reverse=True):
tag = item.data()
if self.key is None: if self.key is None:
self.db.delete_tag(str(item.text())) self.db.delete_tag(tag)
else: else:
bks = self.db.delete_item_from_multiple(str(item.text()), bks = self.db.delete_item_from_multiple(tag, label=self.key)
label=self.key)
self.db.refresh_ids(bks) self.db.refresh_ids(bks)
self.available_tags.takeItem(self.available_tags.row(item)) self.available_tags.model().removeRows(item.row(), 1)
self.available_tags.verticalScrollBar().setValue(pos) self.available_tags.verticalScrollBar().setValue(pos)
def apply_tags(self, item=None): def apply_tags(self, item=None):
items = self.available_tags.selectedItems() if item is None else [item] row_indices = list(self.available_tags.selectionModel().selectedRows())
rows = [self.available_tags.row(i) for i in items] row_indices.sort(key=lambda r: r.row(), reverse=True)
if not rows: if not row_indices:
text = self.available_filter_input.text() text = self.available_filter_input.text()
if text and text.strip(): if text and text.strip():
self.add_tag_input.setText(text) self.add_tag_input.setText(text)
self.add_tag_input.setFocus(Qt.FocusReason.OtherFocusReason) self.add_tag_input.setFocus(Qt.FocusReason.OtherFocusReason)
return return
row = max(rows) pos = self.available_tags.verticalScrollBar().value()
tags = self._get_applied_tags_box_contents() tags = self._get_applied_tags_box_contents()
for item in items: for item in row_indices:
tag = str(item.text()) tag = item.data()
tags.append(tag) tags.append(tag)
self.available_tags.takeItem(self.available_tags.row(item)) self.available_tags.model().removeRows(item.row(), 1)
self.available_tags.verticalScrollBar().setValue(pos)
if not self.is_names: if not self.is_names:
tags.sort(key=sort_key) tags.sort(key=sort_key)
self.applied_tags.clear() self.applied_model.setStringList(tags)
for tag in tags:
self.applied_tags.addItem(tag)
if row >= self.available_tags.count():
row = self.available_tags.count() - 1
if row > 2:
item = self.available_tags.item(row)
self.available_tags.scrollToItem(item)
# use the filter again when the applied tags were changed
self.filter_tags(self.applied_filter_input.text(), which='applied_tags')
def _get_applied_tags_box_contents(self): def _get_applied_tags_box_contents(self):
tags = [] return list(self.applied_model.stringList())
for i in range(0, self.applied_tags.count()):
tags.append(str(self.applied_tags.item(i).text()))
return tags
def unapply_tags(self, item=None): def unapply_tags(self, item=None):
tags = self._get_applied_tags_box_contents() row_indices = list(self.applied_tags.selectionModel().selectedRows())
items = self.applied_tags.selectedItems() if item is None else [item] tags = [r.data() for r in row_indices]
for item in items: row_indices.sort(key=lambda r: r.row(), reverse=True)
tag = str(item.text()) for item in row_indices:
tags.remove(tag) self.applied_model.removeRows(item.row(), 1)
self.available_tags.addItem(tag)
if not self.is_names: all_tags = self.all_tags_model.stringList() + tags
tags.sort(key=sort_key) all_tags.sort(key=sort_key)
self.applied_tags.clear() self.all_tags_model.setStringList(all_tags)
for tag in tags:
self.applied_tags.addItem(tag)
items = [str(self.available_tags.item(x).text()) for x in
range(self.available_tags.count())]
items.sort(key=sort_key)
self.available_tags.clear()
for item in items:
self.available_tags.addItem(item)
# use the filter again when the applied tags were changed
self.filter_tags(self.applied_filter_input.text(), which='applied_tags')
self.filter_tags(self.available_filter_input.text())
def add_tag(self): def add_tag(self):
tags = str(self.add_tag_input.text()).split(self.sep) tags = str(self.add_tag_input.text()).split(self.sep)
@ -216,28 +196,20 @@ class TagEditor(QDialog, Ui_TagEditor):
tag = tag.strip() tag = tag.strip()
if not tag: if not tag:
continue continue
for item in self.available_tags.findItems(tag, Qt.MatchFlag.MatchFixedString): for index in self.all_tags_model.match(self.all_tags_model.index(0), Qt.ItemDataRole.DisplayRole, tag, -1,
self.available_tags.takeItem(self.available_tags.row(item)) Qt.MatchFlag.MatchFixedString | Qt.MatchFlag.MatchCaseSensitive | Qt.MatchFlag.MatchWrap):
self.all_tags_model.removeRow(index.row())
if tag not in tags_in_box: if tag not in tags_in_box:
tags_in_box.append(tag) tags_in_box.append(tag)
if not self.is_names: if not self.is_names:
tags_in_box.sort(key=sort_key) tags_in_box.sort(key=sort_key)
self.applied_tags.clear() self.applied_model.setStringList(tags_in_box)
for tag in tags_in_box:
self.applied_tags.addItem(tag)
self.add_tag_input.setText('') self.add_tag_input.setText('')
# use the filter again when the applied tags were changed
self.filter_tags(self.applied_filter_input.text(), which='applied_tags')
# filter tags
def filter_tags(self, filter_value, which='available_tags'): def filter_tags(self, filter_value, which='available_tags'):
collection = getattr(self, which) collection = getattr(self, which)
q = icu_lower(str(filter_value)) collection.model().setFilterFixedString(filter_value or '')
for i in range(collection.count()): # on every available tag
item = collection.item(i)
item.setHidden(bool(q and not primary_contains(q, str(item.text()))))
def accept(self): def accept(self):
self.tags = self._get_applied_tags_box_contents() self.tags = self._get_applied_tags_box_contents()
@ -258,5 +230,6 @@ if __name__ == '__main__':
db = db() db = db()
app = Application([]) app = Application([])
d = TagEditor(None, db, current_tags='a b c'.split()) d = TagEditor(None, db, current_tags='a b c'.split())
if d.exec() == QDialog.DialogCode.Accepted: if d.exec() == QDialog.DialogCode.Accepted:
print(d.tags) print(d.tags)

View File

@ -94,7 +94,7 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QListWidget" name="available_tags"> <widget class="QListView" name="available_tags">
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -220,7 +220,7 @@
</widget> </widget>
</item> </item>
<item row="2" column="3"> <item row="2" column="3">
<widget class="QListWidget" name="applied_tags"> <widget class="QListView" name="applied_tags">
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>