From 3eb15353ec988d43bb89b76080d5d10242414f60 Mon Sep 17 00:00:00 2001
From: Charles Haley
Date: Wed, 1 Nov 2023 17:11:59 +0000
Subject: [PATCH] Changes to category editors:
- Eliminate the NotesItem class for performance
- Ensure the edited icon is displayed as appropriate
- Add context menus
- Lots of refactoring
- Bug fixes for tag_list... where changes were lost when the list was filtered
Opening authors and tags for the "big library" now takes at most a few seconds.
I will continue to test it. I wouldn't be surprised if I broke something.
---
.../gui2/dialogs/edit_authors_dialog.py | 176 +++--
src/calibre/gui2/dialogs/tag_list_editor.py | 711 ++++++++++++------
2 files changed, 562 insertions(+), 325 deletions(-)
diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.py b/src/calibre/gui2/dialogs/edit_authors_dialog.py
index c678f99660..d290cf90b8 100644
--- a/src/calibre/gui2/dialogs/edit_authors_dialog.py
+++ b/src/calibre/gui2/dialogs/edit_authors_dialog.py
@@ -9,12 +9,13 @@ from contextlib import contextmanager
from functools import partial
from qt.core import (
QAbstractItemView, QAction, QApplication, QDialog, QDialogButtonBox, QFrame, QIcon,
- QLabel, QMenu, QStyledItemDelegate, Qt, QTableWidget, QTableWidgetItem, QTimer,
+ QLabel, QMenu, QStyledItemDelegate, Qt, QTableWidgetItem, QTimer,
)
from calibre.ebooks.metadata import author_to_author_sort, string_to_authors
from calibre.gui2 import error_dialog, gprefs
from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
+from calibre.gui2.dialogs.tag_list_editor import NotesUtilities
from calibre.utils.config import prefs
from calibre.utils.config_base import tweaks
from calibre.utils.icu import (
@@ -25,7 +26,7 @@ from calibre.utils.icu import (
QT_HIDDEN_CLEAR_ACTION = '_q_qlineeditclearaction'
-class tableItem(QTableWidgetItem):
+class TableItem(QTableWidgetItem):
def __init__(self, txt, skey=None):
QTableWidgetItem.__init__(self, txt)
@@ -46,23 +47,30 @@ class tableItem(QTableWidgetItem):
CHECK_MARK = '✓'
+AUTHOR_COLUMN = 0
+AUTHOR_SORT_COLUMN = 1
+LINK_COLUMN = 2
+NOTES_COLUMN = 3
+
class EditColumnDelegate(QStyledItemDelegate):
- def __init__(self, completion_data, parent):
- super().__init__(parent)
+ def __init__(self, completion_data, table, modified_notes, item_id_getter):
+ super().__init__(table)
+ self.table = table
self.completion_data = completion_data
- self.modified_notes = {}
+ self.modified_notes = modified_notes
+ self.item_id_getter = item_id_getter
def createEditor(self, parent, option, index):
- if index.column() == 0:
+ if index.column() == AUTHOR_COLUMN:
if self.completion_data:
from calibre.gui2.complete2 import EditWithComplete
editor = EditWithComplete(parent)
editor.set_separator(None)
editor.update_items_cache(self.completion_data)
return editor
- if index.column() == 3:
+ if index.column() == NOTES_COLUMN:
self.edit_note(self.table.itemFromIndex(index))
return None
@@ -71,37 +79,8 @@ class EditColumnDelegate(QStyledItemDelegate):
editor.setClearButtonEnabled(True)
return editor
- @property
- def table(self) -> QTableWidget:
- return self.parent()
-
- def is_note_modified(self, item_id) -> bool:
- return item_id in self.modified_notes
-
- def undo_note_edit(self, item):
- item_id = int(self.table.item(item.row(), 0).data(Qt.ItemDataRole.UserRole))
- before = self.modified_notes.pop(item_id, None)
- from calibre.gui2.ui import get_gui
- db = get_gui().current_db.new_api
- if before is not None:
- if before:
- db.import_note('authors', item_id, before.encode('utf-8'), path_is_data=True)
- else:
- db.set_notes_for('authors', item_id, '')
-
- def restore_all_notes(self):
- # should only be called from reject()
- from calibre.gui2.ui import get_gui
- db = get_gui().current_db.new_api
- for item_id, before in self.modified_notes.items():
- if before:
- db.import_note('authors', item_id, before.encode('utf-8'), path_is_data=True)
- else:
- db.set_notes_for('authors', item_id, '')
- self.modified_notes.clear()
-
def edit_note(self, item):
- item_id = int(self.table.item(item.row(), 0).data(Qt.ItemDataRole.UserRole))
+ item_id = self.item_id_getter(item)
from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog
from calibre.gui2.ui import get_gui
db = get_gui().current_db.new_api
@@ -136,6 +115,10 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
except Exception:
pass
+ self.modified_notes = {}
+ self.notes_utilities = NotesUtilities(self.table, self.modified_notes, "authors",
+ lambda item: int(self.table.item(item.row(), AUTHOR_COLUMN).data(Qt.ItemDataRole.UserRole)))
+
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(_('&OK'))
self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(_('&Cancel'))
self.buttonBox.accepted.connect(self.accepted)
@@ -228,9 +211,13 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
self.author_sort_order = 0
self.link_order = 1
self.notes_order = 1
- self.table.setItemDelegate(EditColumnDelegate(self.completion_data, self.table))
+ self.table.setItemDelegate(EditColumnDelegate(self.completion_data, self.table,
+ self.modified_notes, self.get_item_id))
self.show_table(id_to_select, select_sort, select_link, is_first_letter)
+ def get_item_id(self, item):
+ return int(self.table.item(item.row(), AUTHOR_COLUMN).data(Qt.ItemDataRole.UserRole))
+
@contextmanager
def no_cell_changed(self):
orig = self.ignore_cell_changed
@@ -274,19 +261,19 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
name, sort, link = (v['name'], v['sort'], v['link'])
name = name.replace('|', ',')
- name_item = tableItem(name)
+ name_item = TableItem(name)
name_item.setData(Qt.ItemDataRole.UserRole, id_)
- sort_item = tableItem(sort)
- link_item = tableItem(link)
+ sort_item = TableItem(sort)
+ link_item = TableItem(link)
- self.table.setItem(row, 0, name_item)
- self.table.setItem(row, 1, sort_item)
- self.table.setItem(row, 2, link_item)
+ self.table.setItem(row, AUTHOR_COLUMN, name_item)
+ self.table.setItem(row, AUTHOR_SORT_COLUMN, sort_item)
+ self.table.setItem(row, LINK_COLUMN, link_item)
if id_ in all_items_that_have_notes:
- note_item = tableItem(yes, yes_skey)
+ note_item = TableItem(yes, yes_skey)
else:
- note_item = tableItem(no, no_skey)
- self.table.setItem(row, 3, note_item)
+ note_item = TableItem(no, no_skey)
+ self.table.setItem(row, NOTES_COLUMN, note_item)
self.set_icon(name_item, id_)
self.set_icon(sort_item, id_)
@@ -315,19 +302,19 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
use_as = tweaks['categories_use_field_for_author_name'] == 'author_sort'
for row in range(0, len(auts_to_show)):
if is_first_letter:
- item_txt = str(self.table.item(row, 1).text() if use_as
- else self.table.item(row, 0).text())
+ item_txt = str(self.table.item(row, AUTHOR_SORT_COLUMN).text() if use_as
+ else self.table.item(row, AUTHOR_COLUMN).text())
if primary_startswith(item_txt, id_to_select):
- select_item = self.table.item(row, 1 if use_as else 0)
+ select_item = self.table.item(row, AUTHOR_SORT_COLUMN if use_as else 0)
break
- elif id_to_select == self.table.item(row, 0).data(Qt.ItemDataRole.UserRole):
+ elif id_to_select == self.table.item(row, AUTHOR_COLUMN).data(Qt.ItemDataRole.UserRole):
if select_sort:
- select_item = self.table.item(row, 1)
+ select_item = self.table.item(row, AUTHOR_SORT_COLUMN)
elif select_link:
- select_item = self.table.item(row, 2)
+ select_item = self.table.item(row, LINK_COLUMN)
else:
- select_item = (self.table.item(row, 1) if use_as
- else self.table.item(row, 0))
+ select_item = (self.table.item(row, AUTHOR_SORT_COLUMN) if use_as
+ else self.table.item(row, AUTHOR_COLUMN))
break
if select_item:
self.table.setCurrentItem(select_item)
@@ -380,8 +367,8 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
def item_is_modified(self, item, id_):
sub = self.get_column_name(item.column())
if sub == 'notes':
- return self.table.itemDelegate().is_note_modified(id_)
- item.text() != self.original_authors[id_][sub]
+ return self.notes_utilities.is_note_modified(id_)
+ return item.text() != self.original_authors[id_][sub]
def show_context_menu(self, point):
self.context_item = self.table.itemAt(point)
@@ -403,34 +390,43 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
m = self.au_context_menu = QMenu(self)
idx = self.table.indexAt(point)
- id_ = int(self.table.item(idx.row(), 0).data(Qt.ItemDataRole.UserRole))
+ id_ = int(self.table.item(idx.row(), AUTHOR_COLUMN).data(Qt.ItemDataRole.UserRole))
sub = self.get_column_name(idx.column())
- if self.context_item is not None and self.item_is_modified(self.context_item, id_):
+ if sub == 'notes':
+ self.notes_utilities.context_menu(m, self.context_item,
+ self.table.item(idx.row(), AUTHOR_COLUMN).text())
+ else:
+ ca = m.addAction(QIcon.ic('edit-copy.png'), _('Copy'))
+ ca.triggered.connect(self.copy_to_clipboard)
+ ca = m.addAction(QIcon.ic('edit-paste.png'), _('Paste'))
+ ca.triggered.connect(self.paste_from_clipboard)
+
ca = m.addAction(QIcon.ic('edit-undo.png'), _('Undo'))
ca.triggered.connect(partial(self.undo_cell,
old_value=self.original_authors[id_].get(sub)))
- m.addSeparator()
- ca = m.addAction(QIcon.ic('edit-copy.png'), _('Copy'))
- ca.triggered.connect(self.copy_to_clipboard)
- ca = m.addAction(QIcon.ic('edit-paste.png'), _('Paste'))
- ca.triggered.connect(self.paste_from_clipboard)
- m.addSeparator()
- if self.context_item is not None and sub == 'name':
- ca = m.addAction(_('Copy to author sort'))
- ca.triggered.connect(self.copy_au_to_aus)
- m.addSeparator()
- ca = m.addAction(QIcon.ic('lt.png'), _("Show books by author in book list"))
- ca.triggered.connect(self.search_in_book_list)
- else:
- ca = m.addAction(_('Copy to author'))
- ca.triggered.connect(self.copy_aus_to_au)
- m.addSeparator()
- m.addMenu(case_menu)
+ ca.setEnabled(self.context_item is not None and self.item_is_modified(self.context_item, id_))
+
+ ca = m.addAction(QIcon.ic('edit_input.png'), _('Edit'))
+ ca.triggered.connect(partial(self.table.editItem, self.context_item))
+
+ if sub != 'link':
+ m.addSeparator()
+ if self.context_item is not None and sub == 'name':
+ ca = m.addAction(_('Copy to author sort'))
+ ca.triggered.connect(self.copy_au_to_aus)
+ m.addSeparator()
+ ca = m.addAction(QIcon.ic('lt.png'), _("Show books by author in book list"))
+ ca.triggered.connect(self.search_in_book_list)
+ else:
+ ca = m.addAction(_('Copy to author'))
+ ca.triggered.connect(self.copy_aus_to_au)
+ m.addSeparator()
+ m.addMenu(case_menu)
m.exec(self.table.viewport().mapToGlobal(point))
def undo_cell(self, old_value):
if self.context_item.column() == 3:
- self.table.itemDelegate().undo_note_edit(self.context_item)
+ self.notes_utilities.undo_note_edit(self.context_item)
else:
self.context_item.setText(old_value)
@@ -438,7 +434,7 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
from calibre.gui2.ui import get_gui
row = self.context_item.row()
get_gui().search.set_search_string('authors:="%s"' %
- str(self.table.item(row, 0).text()).replace(r'"', r'\"'))
+ str(self.table.item(row, AUTHOR_COLUMN).text()).replace(r'"', r'\"'))
def copy_to_clipboard(self):
cb = QApplication.clipboard()
@@ -467,12 +463,12 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
def copy_aus_to_au(self):
row = self.context_item.row()
- dest = self.table.item(row, 0)
+ dest = self.table.item(row, AUTHOR_COLUMN)
dest.setText(self.context_item.text())
def copy_au_to_aus(self):
row = self.context_item.row()
- dest = self.table.item(row, 1)
+ dest = self.table.item(row, AUTHOR_SORT_COLUMN)
dest.setText(self.context_item.text())
def not_found_label_timer_event(self):
@@ -544,16 +540,16 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
self.result.append((id_, orig['name'], v['name'], v['sort'], v['link']))
def rejected(self):
- self.table.itemDelegate().restore_all_notes()
+ self.notes_utilities.restore_all_notes()
self.save_state()
def do_recalc_author_sort(self):
with self.no_cell_changed():
for row in range(0,self.table.rowCount()):
- item_aut = self.table.item(row, 0)
+ item_aut = self.table.item(row, AUTHOR_COLUMN)
id_ = int(item_aut.data(Qt.ItemDataRole.UserRole))
aut = str(item_aut.text()).strip()
- item_aus = self.table.item(row, 1)
+ item_aus = self.table.item(row, AUTHOR_SORT_COLUMN)
# Sometimes trailing commas are left by changing between copy algs
aus = str(author_to_author_sort(aut)).rstrip(',')
item_aus.setText(aus)
@@ -564,8 +560,8 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
def do_auth_sort_to_author(self):
with self.no_cell_changed():
for row in range(0,self.table.rowCount()):
- aus = str(self.table.item(row, 1).text()).strip()
- item_aut = self.table.item(row, 0)
+ aus = str(self.table.item(row, AUTHOR_SORT_COLUMN).text()).strip()
+ item_aut = self.table.item(row, AUTHOR_COLUMN)
id_ = int(item_aut.data(Qt.ItemDataRole.UserRole))
item_aut.setText(aus)
self.authors[id_]['name'] = aus
@@ -580,20 +576,20 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
if self.ignore_cell_changed:
return
with self.no_cell_changed():
- id_ = int(self.table.item(row, 0).data(Qt.ItemDataRole.UserRole))
- if col == 0:
- item = self.table.item(row, 0)
+ id_ = int(self.table.item(row, AUTHOR_COLUMN).data(Qt.ItemDataRole.UserRole))
+ if col == AUTHOR_COLUMN:
+ item = self.table.item(row, AUTHOR_COLUMN)
aut = str(item.text()).strip()
aut_list = string_to_authors(aut)
if len(aut_list) != 1:
error_dialog(self.parent(), _('Invalid author name'),
_('You cannot change an author to multiple authors.')).exec()
aut = ' % '.join(aut_list)
- self.table.item(row, 0).setText(aut)
+ self.table.item(row, AUTHOR_COLUMN).setText(aut)
item.set_sort_key()
self.authors[id_]['name'] = aut
self.set_icon(item, id_)
- c = self.table.item(row, 1)
+ c = self.table.item(row, AUTHOR_SORT_COLUMN)
txt = author_to_author_sort(aut)
self.authors[id_]['sort'] = txt
c.setText(txt) # This triggers another cellChanged event
diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py
index 946010afea..5b9b2d89e3 100644
--- a/src/calibre/gui2/dialogs/tag_list_editor.py
+++ b/src/calibre/gui2/dialogs/tag_list_editor.py
@@ -2,14 +2,18 @@
# License: GPLv3 Copyright: 2008, Kovid Goyal
+import copy
+from contextlib import contextmanager
from functools import partial
+
from qt.core import (
- QAbstractItemView, QAction, QApplication, QCheckBox, QColor, QDialog,
- QDialogButtonBox, QEvent, QFrame, QIcon, QLabel, QMenu, QSize, QStyledItemDelegate,
- Qt, QTableWidgetItem, QTimer, QToolButton, pyqtSignal, sip,
+ QAbstractItemView, QAction, QApplication, QColor, QDialog,
+ QDialogButtonBox, QFrame, QIcon, QLabel, QMenu, QSize, QStyledItemDelegate,
+ Qt, QTableWidgetItem, QTimer, pyqtSignal, sip,
)
-from calibre.gui2 import error_dialog, gprefs, question_dialog
+from calibre import sanitize_file_name
+from calibre.gui2 import error_dialog, gprefs, question_dialog, choose_files, choose_save_file
from calibre.gui2.actions.show_quickview import get_quickview_action_plugin
from calibre.gui2.complete2 import EditWithComplete
from calibre.gui2.dialogs.confirm_delete import confirm
@@ -66,6 +70,9 @@ class NameTableWidgetItem(QTableWidgetItem):
def initial_text(self):
return self.initial_value
+ def text_is_modified(self):
+ return not self.is_deleted and self.current_value != self.initial_value
+
def text(self):
return self.current_value
@@ -109,21 +116,137 @@ class CountTableWidgetItem(QTableWidgetItem):
return self._count < other._count
+class NotesUtilities():
+
+ def __init__(self, table, modified_notes, category, item_id_getter):
+ self.table = table
+ self.modified_notes = modified_notes
+ self.category = category
+ self.item_id_getter = item_id_getter
+
+ def is_note_modified(self, item_id) -> bool:
+ return item_id in self.modified_notes
+
+ def get_db(self):
+ from calibre.gui2.ui import get_gui
+ return get_gui().current_db.new_api
+
+ def restore_all_notes(self):
+ # should only be called from reject()
+ db = self.get_db()
+ for item_id, before in self.modified_notes.items():
+ if before:
+ db.import_note(self.category, item_id, before.encode('utf-8'), path_is_data=True)
+ else:
+ db.set_notes_for(self.category, item_id, '')
+ self.modified_notes.clear()
+
+ def undo_note_edit(self, item):
+ item_id = self.item_id_getter(item)
+ before = self.modified_notes.pop(item_id, None)
+ db = self.get_db()
+ if before is not None:
+ if before:
+ db.import_note(self.category, item_id, before.encode('utf-8'), path_is_data=True)
+ else:
+ db.set_notes_for(self.category, item_id, '')
+ item.setText(CHECK_MARK if before else '')
+ item.setIcon(QIcon())
+
+ def delete_note(self, item):
+ item_id = self.item_id_getter(item)
+ db = self.get_db()
+ if item_id not in self.modified_notes:
+ self.modified_notes[item_id] = db.notes_for(self.category, item_id)
+ db.set_notes_for(self.category, item_id, '')
+ item.setText('')
+ self.table.cellChanged.emit(item.row(), item.column())
+ self.table.itemChanged.emit(item)
+
+ def do_export(self, item, item_name):
+ item_id = self.item_id_getter(item)
+ dest = choose_save_file(self.table, 'save-exported-note', _('Export note to a file'),
+ filters=[(_('HTML files'), ['html'])],
+ initial_filename=f'{sanitize_file_name(item_name)}.html',
+ all_files=False)
+ if dest:
+ html = self.get_db().export_note(self.category, item_id)
+ with open(dest, 'wb') as f:
+ f.write(html.encode('utf-8'))
+
+ def do_import(self, item):
+ src = choose_files(self.table, 'load-imported-note', _('Import note from a file'),
+ filters=[(_('HTML files'), ['html'])],
+ all_files=False, select_only_single_file=True)
+ if src:
+ item_id = self.item_id_getter(item)
+ db = self.get_db()
+ before = db.notes_for(self.category, item_id)
+ if item_id not in self.modified_notes:
+ self.modified_notes[item_id] = before
+ db.import_note(self.category, item_id, src[0])
+ after = db.notes_for(self.category, item_id)
+ item.setText(CHECK_MARK if after else '')
+ self.table.cellChanged.emit(item.row(), item.column())
+ self.table.itemChanged.emit(item)
+
+ edit_icon = QIcon.ic('edit_input.png')
+ delete_icon = QIcon.ic('trash.png')
+ undo_delete_icon = QIcon.ic('edit-undo.png')
+ export_icon = QIcon.ic('forward.png')
+ import_icon = QIcon.ic('back.png')
+
+ def context_menu(self, menu, item, item_name):
+ m = menu
+ item_id = self.item_id_getter(item)
+ from calibre.gui2.ui import get_gui
+ db = get_gui().current_db.new_api
+ has_note = bool(db.notes_for(self.category, item_id))
+
+ ac = m.addAction(self.undo_delete_icon, _('Undo'))
+ ac.setEnabled(item_id in self.modified_notes)
+ ac.triggered.connect(partial(self.undo_note_edit, item))
+
+ ac = m.addAction(self.edit_icon, _('Edit note') if has_note else _('Create note'))
+ ac.triggered.connect(partial(self.table.editItem, item))
+
+ ac = m.addAction(self.delete_icon, _('Delete note'))
+ ac.setEnabled(has_note)
+ ac.triggered.connect(partial(self.delete_note, item))
+
+ ac = m.addAction(self.export_icon, _('Export note to a file'))
+ ac.setEnabled(has_note)
+ ac.triggered.connect(partial(self.do_export, item, item_name))
+
+ ac = m.addAction(self.import_icon, _('Import note from a file'))
+ ac.triggered.connect(partial(self.do_import, item))
+
+
+VALUE_COLUMN = 0
+COUNT_COLUMN = 1
+WAS_COLUMN = 2
+LINK_COLUMN = 3
+NOTES_COLUMN = 4
+
+
class EditColumnDelegate(QStyledItemDelegate):
editing_finished = pyqtSignal(int)
editing_started = pyqtSignal(int)
- def __init__(self, table, check_for_deleted_items, parent=None):
+ def __init__(self, table, check_for_deleted_items, category, modified_notes, item_id_getter, parent=None):
super().__init__(table)
self.table = table
self.completion_data = None
self.check_for_deleted_items = check_for_deleted_items
+ self.category = category
+ self.modified_notes = modified_notes
+ self.item_id_getter = item_id_getter
def set_completion_data(self, data):
self.completion_data = data
def createEditor(self, parent, option, index):
- if index.column() == 0:
+ if index.column() == VALUE_COLUMN:
if self.check_for_deleted_items(show_error=True):
return None
self.editing_started.emit(index.row())
@@ -135,6 +258,9 @@ class EditColumnDelegate(QStyledItemDelegate):
else:
editor = EnLineEdit(parent)
return editor
+ if index.column() == NOTES_COLUMN:
+ self.edit_note(self.table.itemFromIndex(index))
+ return None
self.editing_started.emit(index.row())
editor = EnLineEdit(parent)
editor.setClearButtonEnabled(True)
@@ -144,39 +270,34 @@ class EditColumnDelegate(QStyledItemDelegate):
self.editing_finished.emit(index.row())
super().destroyEditor(editor, index)
-
-# These My... classes are needed to make context menus work on disabled widgets
-
-def event(ev, me=None, super_class=None, context_menu_handler=None):
- if not me.isEnabled() and ev.type() == QEvent.MouseButtonRelease:
- if ev.button() == Qt.MouseButton.RightButton:
- # let the event finish before the context menu is opened.
- QTimer.singleShot(0, partial(context_menu_handler, ev.position().toPoint()))
- return True
- # if the widget is enabled then it handles its own context menu events
- return super_class.event(ev)
+ def edit_note(self, item):
+ item_id = self.item_id_getter(item)
+ from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog
+ from calibre.gui2.ui import get_gui
+ db = get_gui().current_db.new_api
+ before = db.notes_for(self.category, item_id)
+ note = db.export_note(self.category, item_id) if before else ''
+ d = EditNoteDialog(self.category, item_id, db, parent=self.table)
+ if d.exec() == QDialog.DialogCode.Accepted:
+ after = db.notes_for(self.category, item_id)
+ if item_id not in self.modified_notes:
+ self.modified_notes[item_id] = note
+ item.setText(CHECK_MARK if after else '')
+ self.table.cellChanged.emit(item.row(), item.column())
+ self.table.itemChanged.emit(item)
-class MyToolButton(QToolButton):
-
- def __init__(self, context_menu_handler):
- QToolButton.__init__(self)
- self.event = partial(event, me=self, super_class=super(), context_menu_handler=context_menu_handler)
-
-
-class MyCheckBox(QCheckBox):
-
- def __init__(self, context_menu_handler):
- QCheckBox.__init__(self)
- self.event = partial(event, me=self, super_class=super(), context_menu_handler=context_menu_handler)
+@contextmanager
+def block_signals(widget):
+ old = widget.blockSignals(True)
+ try:
+ yield
+ finally:
+ widget.blockSignals(old)
class TagListEditor(QDialog, Ui_TagListEditor):
- VALUE_COLUMN = 0
- LINK_COLUMN = 3
- NOTES_COLUMN = 4
-
def __init__(self, window, cat_name, tag_to_match, get_book_ids, sorter,
ttm_is_first_letter=False, category=None, fm=None, link_map=None):
QDialog.__init__(self, window)
@@ -186,7 +307,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.setupUi(self)
self.verticalLayout_2.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.search_box.setMinimumContentsLength(25)
- self.link_map = link_map
+ if category is not None:
+ item_map = get_gui().current_db.new_api.get_item_name_map(category)
+ self.original_links = {item_map[k]:v for k,v in link_map.items()}
+ self.current_links = copy.copy(self.original_links)
+ else:
+ self.original_links = {}
+ self.current_links = {}
# Put the category name into the title bar
t = self.windowTitle()
@@ -198,12 +325,15 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.setWindowFlags(self.windowFlags()&(~Qt.WindowType.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
+ self.edited_icon = QIcon.ic('modified.png')
+
# initialization
self.to_rename = {}
self.to_delete = set()
self.all_tags = {}
self.original_names = {}
self.links = {}
+ self.modified_notes = {}
self.ordered_tags = []
self.sorter = sorter
@@ -259,51 +389,88 @@ class TagListEditor(QDialog, Ui_TagListEditor):
def sizeHint(self):
return super().sizeHint() + QSize(150, 100)
- def show_context_menu(self, point):
- item = self.table.itemAt(point)
- if item is None or item.column() != self.VALUE_COLUMN:
- return
- m = self.au_context_menu = QMenu(self)
+ def link_context_menu(self, menu, item):
+ m = menu
+ is_deleted = bool(self.table.item(item.row(), VALUE_COLUMN).is_deleted)
+ item_id = self.get_item_id(item)
- disable_copy_paste_search = len(self.table.selectedItems()) != 1 or item.is_deleted
ca = m.addAction(_('Copy'))
ca.triggered.connect(partial(self.copy_to_clipboard, item))
ca.setIcon(QIcon.ic('edit-copy.png'))
- if disable_copy_paste_search:
- ca.setEnabled(False)
+ ca.setEnabled(not is_deleted)
+
ca = m.addAction(_('Paste'))
ca.setIcon(QIcon.ic('edit-paste.png'))
ca.triggered.connect(partial(self.paste_from_clipboard, item))
- if disable_copy_paste_search:
- ca.setEnabled(False)
+ ca.setEnabled(not is_deleted)
+
ca = m.addAction(_('Undo'))
ca.setIcon(QIcon.ic('edit-undo.png'))
- ca.triggered.connect(self.undo_edit)
- ca.setEnabled(False)
- for item in self.table.selectedItems():
- if (item.text() != self.original_names[int(item.data(Qt.ItemDataRole.UserRole))] or item.is_deleted):
- ca.setEnabled(True)
- break
+ ca.triggered.connect(partial(self.undo_link_edit, item, item_id))
+ ca.setEnabled(not is_deleted and self.link_is_edited(item_id))
+
+ ca = m.addAction(_('Edit'))
+ ca.setIcon(QIcon.ic('edit_input.png'))
+ ca.triggered.connect(partial(self.table.editItem, item))
+ ca.setEnabled(not is_deleted)
+
+ ca = m.addAction(_('Delete link'))
+ ca.setIcon(QIcon.ic('trash.png'))
+ def delete_link_text(item):
+ item.setText('')
+ ca.triggered.connect(partial(delete_link_text, item))
+ ca.setEnabled(not is_deleted)
+
+ def value_context_menu(self, menu, item):
+ m = menu
+ self.table.setCurrentItem(item)
+
+ ca = m.addAction(_('Copy'))
+ ca.triggered.connect(partial(self.copy_to_clipboard, item))
+ ca.setIcon(QIcon.ic('edit-copy.png'))
+ ca.setEnabled(not item.is_deleted)
+
+ ca = m.addAction(_('Paste'))
+ ca.setIcon(QIcon.ic('edit-paste.png'))
+ ca.triggered.connect(partial(self.paste_from_clipboard, item))
+ ca.setEnabled(not item.is_deleted)
+
+ ca = m.addAction(_('Undo'))
+ ca.setIcon(QIcon.ic('edit-undo.png'))
+ if item.is_deleted:
+ ca.triggered.connect(self.undo_edit)
+ else:
+ ca.triggered.connect(partial(self.undo_value_edit, item, self.get_item_id(item)))
+ ca.setEnabled(item.is_deleted or item.text() != self.original_names[self.get_item_id(item)])
+
ca = m.addAction(_('Edit'))
ca.setIcon(QIcon.ic('edit_input.png'))
ca.triggered.connect(self.rename_tag)
+ ca.setEnabled(not item.is_deleted)
+
ca = m.addAction(_('Delete'))
ca.setIcon(QIcon.ic('trash.png'))
ca.triggered.connect(self.delete_tags)
item_name = str(item.text())
+ ca.setEnabled(not item.is_deleted)
+
ca = m.addAction(_('Search for {}').format(item_name))
ca.setIcon(QIcon.ic('search.png'))
ca.triggered.connect(partial(self.set_search_text, item_name))
item_name = str(item.text())
+ ca.setEnabled(not item.is_deleted)
+
ca = m.addAction(_('Filter by {}').format(item_name))
ca.setIcon(QIcon.ic('filter.png'))
ca.triggered.connect(partial(self.set_filter_text, item_name))
+ ca.setEnabled(not item.is_deleted)
+
if self.category is not None:
ca = m.addAction(_("Search the library for {0}").format(item_name))
ca.setIcon(QIcon.ic('lt.png'))
ca.triggered.connect(partial(self.search_for_books, item))
- if disable_copy_paste_search:
- ca.setEnabled(False)
+ ca.setEnabled(not item.is_deleted)
+
if self.table.state() == QAbstractItemView.State.EditingState:
m.addSeparator()
case_menu = QMenu(_('Change case'))
@@ -319,6 +486,18 @@ class TagListEditor(QDialog, Ui_TagListEditor):
action_title_case.triggered.connect(partial(self.do_case, titlecase))
action_capitalize.triggered.connect(partial(self.do_case, capitalize))
m.addMenu(case_menu)
+
+ def show_context_menu(self, point):
+ item = self.table.itemAt(point)
+ if item is None or item.column() in (WAS_COLUMN, COUNT_COLUMN):
+ return
+ m = QMenu()
+ if item.column() == NOTES_COLUMN:
+ self.notes_utilities.context_menu(m, item, self.table.item(item.row(), VALUE_COLUMN).text())
+ elif item.column() == VALUE_COLUMN:
+ self.value_context_menu(m, item)
+ elif item.column() == LINK_COLUMN:
+ self.link_context_menu(m, item)
m.exec(self.table.viewport().mapToGlobal(point))
def search_for_books(self, item):
@@ -352,10 +531,9 @@ class TagListEditor(QDialog, Ui_TagListEditor):
def do_case(self, func):
items = self.table.selectedItems()
# block signals to avoid the "edit one changes all" behavior
- self.table.blockSignals(True)
- for item in items:
- item.setText(func(str(item.text())))
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ for item in items:
+ item.setText(func(str(item.text())))
def swap_case(self, txt):
return txt.swapcase()
@@ -371,8 +549,8 @@ class TagListEditor(QDialog, Ui_TagListEditor):
return
for _ in range(0, self.table.rowCount()):
r = self.search_item_row = (self.search_item_row + 1) % self.table.rowCount()
- if self.string_contains(find_text, self.table.item(r, self.VALUE_COLUMN).text()):
- self.table.setCurrentItem(self.table.item(r, self.VALUE_COLUMN))
+ if self.string_contains(find_text, self.table.item(r, VALUE_COLUMN).text()):
+ self.table.setCurrentItem(self.table.item(r, VALUE_COLUMN))
self.table.setFocus(Qt.FocusReason.OtherFocusReason)
return
# Nothing found. Pop up the little dialog for 1.5 seconds
@@ -419,11 +597,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.setColumnCount(5)
- self.edit_delegate = EditColumnDelegate(self.table, self.check_for_deleted_items)
+ self.edit_delegate = EditColumnDelegate(self.table, self.check_for_deleted_items,
+ self.category, self.modified_notes, self.get_item_id)
self.edit_delegate.editing_finished.connect(self.stop_editing)
self.edit_delegate.editing_started.connect(self.start_editing)
- self.table.setItemDelegateForColumn(self.VALUE_COLUMN, self.edit_delegate)
- self.table.setItemDelegateForColumn(self.LINK_COLUMN, self.edit_delegate)
+ self.table.setItemDelegateForColumn(VALUE_COLUMN, self.edit_delegate)
+ self.table.setItemDelegateForColumn(LINK_COLUMN, self.edit_delegate)
+ self.table.setItemDelegateForColumn(NOTES_COLUMN, self.edit_delegate)
self.table.delete_pressed.connect(self.delete_pressed)
self.table.itemDoubleClicked.connect(self._rename_tag)
@@ -454,10 +634,24 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.show_context_menu)
+ self.notes_utilities = NotesUtilities(self.table, self.modified_notes, self.category, self.get_item_id)
+
+ def get_item_id(self, item):
+ return int(self.table.item(item.row(), VALUE_COLUMN).data(Qt.ItemDataRole.UserRole))
def row_height_changed(self, row, old, new):
self.table.verticalHeader().setDefaultSectionSize(new)
+ def link_is_edited(self, item_id):
+ return self.current_links.get(item_id, None) != self.original_links.get(item_id)
+
+ def set_link_icon(self, id_, item):
+ with block_signals(self.table):
+ if self.link_is_edited(id_):
+ item.setIcon(self.edited_icon)
+ else:
+ item.setIcon(QIcon())
+
def fill_in_table(self, tags, tag_to_match, ttm_is_first_letter):
self.create_table()
@@ -479,90 +673,96 @@ class TagListEditor(QDialog, Ui_TagListEditor):
tags = self.ordered_tags
select_item = None
- self.table.blockSignals(True)
- self.name_col = QTableWidgetItem(self.category_name)
- self.table.setHorizontalHeaderItem(self.VALUE_COLUMN, self.name_col)
- self.count_col = QTableWidgetItem(_('Count'))
- self.table.setHorizontalHeaderItem(1, self.count_col)
- self.was_col = QTableWidgetItem(_('Was'))
- self.table.setHorizontalHeaderItem(2, self.was_col)
- self.link_col = QTableWidgetItem(_('Link'))
- self.table.setHorizontalHeaderItem(self.LINK_COLUMN, self.link_col)
- if self.supports_notes:
- self.notes_col = QTableWidgetItem(_('Notes'))
- self.table.setHorizontalHeaderItem(4, self.notes_col)
-
- self.table.setRowCount(len(tags))
- if self.supports_notes:
- from calibre.gui2.ui import get_gui
- all_items_that_have_notes = get_gui().current_db.new_api.get_all_items_that_have_notes(self.category)
- for row,tag in enumerate(tags):
- item = NameTableWidgetItem(self.sorter)
- is_deleted = self.all_tags[tag]['is_deleted']
- item.set_is_deleted(is_deleted)
- _id = self.all_tags[tag]['key']
- item.setData(Qt.ItemDataRole.UserRole, _id)
- item.set_initial_text(tag)
- if _id in self.to_rename:
- item.setText(self.to_rename[_id])
- else:
- item.setText(tag)
- if self.is_enumerated and str(item.text()) not in self.enum_permitted_values:
- item.setBackground(QColor('#FF2400'))
- item.setToolTip(
- '' +
- _("This is not one of this column's permitted values ({0})"
- ).format(', '.join(self.enum_permitted_values)) + '
')
- item.setFlags(item.flags() | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable)
- self.table.setItem(row, self.VALUE_COLUMN, item)
- if select_item is None:
- if ttm_is_first_letter:
- if primary_startswith(tag, tag_to_match):
- select_item = item
- elif tag == tag_to_match:
- select_item = item
- item = CountTableWidgetItem(self.all_tags[tag]['count'])
- # only the name column can be selected
- item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
- self.table.setItem(row, 1, item)
-
- item = QTableWidgetItem()
- item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
- if _id in self.to_rename or _id in self.to_delete:
- item.setData(Qt.ItemDataRole.DisplayRole, tag)
- self.table.setItem(row, 2, item)
-
- item = QTableWidgetItem()
- if self.link_map is None:
- item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
- item.setText(_('no links available'))
- else:
- if is_deleted:
- item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
- item.setIcon(QIcon.ic('trash.png'))
- else:
- item.setFlags(item.flags() | (Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
- item.setIcon(QIcon())
- item.setText(self.link_map.get(tag, ''))
- self.table.setItem(row, self.LINK_COLUMN, item)
-
+ with block_signals(self.table):
+ self.name_col = QTableWidgetItem(self.category_name)
+ self.table.setHorizontalHeaderItem(VALUE_COLUMN, self.name_col)
+ self.count_col = QTableWidgetItem(_('Count'))
+ self.table.setHorizontalHeaderItem(1, self.count_col)
+ self.was_col = QTableWidgetItem(_('Was'))
+ self.table.setHorizontalHeaderItem(2, self.was_col)
+ self.link_col = QTableWidgetItem(_('Link'))
+ self.table.setHorizontalHeaderItem(LINK_COLUMN, self.link_col)
if self.supports_notes:
- self.table.setItem(row, self.NOTES_COLUMN, QTableWidgetItem(CHECK_MARK if _id in all_items_that_have_notes else ''))
+ self.notes_col = QTableWidgetItem(_('Notes'))
+ self.table.setHorizontalHeaderItem(4, self.notes_col)
- # re-sort the table
- column = self.sort_names.index(self.last_sorted_by)
- sort_order = getattr(self, self.last_sorted_by + '_order')
- self.table.sortByColumn(column, Qt.SortOrder(sort_order))
+ self.table.setRowCount(len(tags))
+ if self.supports_notes:
+ from calibre.gui2.ui import get_gui
+ all_items_that_have_notes = get_gui().current_db.new_api.get_all_items_that_have_notes(self.category)
+ for row,tag in enumerate(tags):
+ item = NameTableWidgetItem(self.sorter)
+ is_deleted = self.all_tags[tag]['is_deleted']
+ item.set_is_deleted(is_deleted)
+ id_ = self.all_tags[tag]['key']
+ item.setData(Qt.ItemDataRole.UserRole, id_)
+ item.set_initial_text(tag)
+ if id_ in self.to_rename:
+ item.setText(self.to_rename[id_])
+ else:
+ item.setText(tag)
+ if self.is_enumerated and str(item.text()) not in self.enum_permitted_values:
+ item.setBackground(QColor('#FF2400'))
+ item.setToolTip(
+ '' +
+ _("This is not one of this column's permitted values ({0})"
+ ).format(', '.join(self.enum_permitted_values)) + '
')
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable)
+ self.table.setItem(row, VALUE_COLUMN, item)
+ if select_item is None:
+ if ttm_is_first_letter:
+ if primary_startswith(tag, tag_to_match):
+ select_item = item
+ elif tag == tag_to_match:
+ select_item = item
+ if item.text_is_modified():
+ item.setIcon(self.edited_icon)
- if select_item is not None:
- self.table.setCurrentItem(select_item)
- self.table.setFocus(Qt.FocusReason.OtherFocusReason)
- self.start_find_pos = select_item.row()
- else:
- self.table.setCurrentCell(0, 0)
- self.search_box.setFocus()
- self.start_find_pos = -1
- self.table.blockSignals(False)
+ item = CountTableWidgetItem(self.all_tags[tag]['count'])
+ item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
+ self.table.setItem(row, COUNT_COLUMN, item)
+
+ item = QTableWidgetItem()
+ item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
+ if id_ in self.to_rename or id_ in self.to_delete:
+ item.setData(Qt.ItemDataRole.DisplayRole, tag)
+ self.table.setItem(row, WAS_COLUMN, item)
+
+ item = QTableWidgetItem()
+ if self.original_links is None:
+ item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
+ item.setText(_('no links available'))
+ else:
+ if is_deleted:
+ item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
+ else:
+ item.setFlags(item.flags() | (Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
+ self.set_link_icon(id_, item)
+ item.setText(self.current_links.get(id_, ''))
+ self.table.setItem(row, LINK_COLUMN, item)
+
+ if self.supports_notes:
+ item = QTableWidgetItem(CHECK_MARK if id_ in all_items_that_have_notes else '')
+ if is_deleted:
+ item.setFlags(item.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
+ else:
+ item.setFlags(item.flags() | (Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
+ item.setIcon(self.edited_icon if id_ in self.modified_notes else QIcon())
+ self.table.setItem(row, NOTES_COLUMN, item)
+
+ # re-sort the table
+ column = self.sort_names.index(self.last_sorted_by)
+ sort_order = getattr(self, self.last_sorted_by + '_order')
+ self.table.sortByColumn(column, Qt.SortOrder(sort_order))
+
+ if select_item is not None:
+ self.table.setCurrentItem(select_item)
+ self.table.setFocus(Qt.FocusReason.OtherFocusReason)
+ self.start_find_pos = select_item.row()
+ else:
+ self.table.setCurrentCell(0, 0)
+ self.search_box.setFocus()
+ self.start_find_pos = -1
def not_found_label_timer_event(self):
self.not_found_label.setVisible(False)
@@ -597,48 +797,54 @@ class TagListEditor(QDialog, Ui_TagListEditor):
for c in range(0, self.table.columnCount()):
self.table.setColumnWidth(c, w)
- def save_geometry(self):
- gprefs['general_category_editor_row_height'] = self.table.verticalHeader().defaultSectionSize()
- gprefs['tag_list_editor_table_widths'] = self.table_column_widths
- super().save_geometry(gprefs, 'tag_list_editor_dialog_geometry')
-
def start_editing(self, on_row):
current_column = self.table.currentItem().column()
- # We don't support editing multiple link rows at the same time. Use
- # the current cell.
- if current_column != self.VALUE_COLUMN:
+ # We don't support editing multiple link or notes rows at the same time.
+ # Use the current cell.
+ if current_column != VALUE_COLUMN:
self.table.setCurrentItem(self.table.item(on_row, current_column))
items = self.table.selectedItems()
- self.table.blockSignals(True)
- self.table.setSortingEnabled(False)
- for item in items:
- if item.row() != on_row:
- item.set_placeholder(_('Editing...'))
- else:
- self.text_before_editing = item.text()
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ self.table.setSortingEnabled(False)
+ for item in items:
+ if item.row() != on_row:
+ item.set_placeholder(_('Editing...'))
+ else:
+ self.text_before_editing = item.text()
def stop_editing(self, on_row):
- # This works because the link field doesn't support editing on multiple
- # lines, so the on_row check will always be false.
+ # This works because the link and notes fields doesn't support editing
+ # on multiple lines, so the on_row check will always be false.
items = self.table.selectedItems()
- self.table.blockSignals(True)
- for item in items:
- if item.row() != on_row and item.is_placeholder:
- item.reset_placeholder()
- self.table.setSortingEnabled(True)
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ for item in items:
+ if item.row() != on_row and item.is_placeholder:
+ item.reset_placeholder()
+ self.table.setSortingEnabled(True)
def finish_editing(self, edited_item):
- if edited_item.column() != self.VALUE_COLUMN:
- # Nothing to do for link fields
+ if edited_item.column() == LINK_COLUMN:
+ id_ = self.get_item_id(edited_item)
+ txt = edited_item.text()
+ if txt:
+ self.current_links[id_] = txt
+ else:
+ self.current_links.pop(id_, None)
+ self.set_link_icon(id_, edited_item)
return
+
+ if edited_item.column() == NOTES_COLUMN:
+ id_ = self.get_item_id(edited_item)
+ with block_signals(self.table):
+ edited_item.setIcon(self.edited_icon if id_ in self.modified_notes else QIcon())
+ return
+
+ # Item value column
if not edited_item.text():
error_dialog(self, _('Item is blank'), _(
'An item cannot be set to nothing. Delete it instead.'), show=True)
- self.table.blockSignals(True)
- edited_item.setText(self.text_before_editing)
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ edited_item.setText(self.text_before_editing)
return
new_text = str(edited_item.text())
if self.is_enumerated and new_text not in self.enum_permitted_values:
@@ -646,58 +852,84 @@ class TagListEditor(QDialog, Ui_TagListEditor):
"This column has a fixed set of permitted values. The entered "
"text must be one of ({0}).").format(', '.join(self.enum_permitted_values)) +
'
', show=True)
- self.table.blockSignals(True)
- edited_item.setText(self.text_before_editing)
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ edited_item.setText(self.text_before_editing)
return
items = self.table.selectedItems()
- self.table.blockSignals(True)
- for item in items:
- id_ = int(item.data(Qt.ItemDataRole.UserRole))
- self.to_rename[id_] = new_text
- orig = self.table.item(item.row(), 2)
- item.setText(new_text)
- orig.setData(Qt.ItemDataRole.DisplayRole, item.initial_text())
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ for item in items:
+ id_ = int(item.data(Qt.ItemDataRole.UserRole))
+ self.to_rename[id_] = new_text
+ orig = self.table.item(item.row(), WAS_COLUMN)
+ item.setText(new_text)
+ if item.text_is_modified():
+ item.setIcon(self.edited_icon)
+ orig.setData(Qt.ItemDataRole.DisplayRole, item.initial_text())
+ else:
+ item.setIcon(QIcon())
+ orig.setData(Qt.ItemDataRole.DisplayRole, '')
+
+ def undo_link_edit(self, item, item_id):
+ if item_id in self.original_links:
+ link_txt = self.current_links[item_id] = self.original_links[item_id]
+ else:
+ self.current_links.pop(item_id, None)
+ link_txt = ''
+ item = self.table.item(item.row(), LINK_COLUMN)
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsSelectable)
+ item.setText(link_txt)
+ item.setIcon(QIcon())
+
+ def undo_value_edit(self, item, item_id):
+ with block_signals(self.table):
+ item.setText(item.initial_text())
+ self.to_rename.pop(item_id, None)
+ row = item.row()
+ self.table.item(row, WAS_COLUMN).setData(Qt.ItemDataRole.DisplayRole, '')
+ item.setIcon(self.edited_icon if item.text_is_modified() else QIcon())
def undo_edit(self):
- col_zero_items = (self.table.item(item.row(), self.VALUE_COLUMN) for item in self.table.selectedItems())
+ col_zero_items = (self.table.item(item.row(), VALUE_COLUMN) for item in self.table.selectedItems())
if not col_zero_items:
error_dialog(self, _('No item selected'),
_('You must select one item from the list of available items.')).exec()
return
if not confirm(
- _('Do you really want to undo your changes?'),
+ _('Do you really want to undo all your changes on selected rows?'),
'tag_list_editor_undo'):
return
- self.table.blockSignals(True)
- for col_zero_item in col_zero_items:
- col_zero_item.setText(col_zero_item.initial_text())
- col_zero_item.set_is_deleted(False)
- self.to_delete.discard(int(col_zero_item.data(Qt.ItemDataRole.UserRole)))
- self.to_rename.pop(int(col_zero_item.data(Qt.ItemDataRole.UserRole)), None)
- row = col_zero_item.row()
- self.table.item(row, 2).setData(Qt.ItemDataRole.DisplayRole, '')
- item = self.table.item(row, self.LINK_COLUMN)
- item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsSelectable)
- item.setIcon(QIcon())
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ for col_zero_item in col_zero_items:
+ id_ = self.get_item_id(col_zero_item)
+ row = col_zero_item.row()
+
+ # item value column
+ self.undo_value_edit(col_zero_item, id_)
+ col_zero_item.set_is_deleted(False)
+ self.to_delete.discard(id_)
+
+ # Link column
+ self.undo_link_edit(self.table.item(row, LINK_COLUMN), id_)
+ # Notes column
+ item = self.table.item(row, NOTES_COLUMN)
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsSelectable)
+ if id_ in self.modified_notes:
+ self.notes_utilities.undo_note_edit(item)
def selection_changed(self):
if self.table.currentIndex().isValid():
col = self.table.currentIndex().column()
- self.table.blockSignals(True)
- if col == self.NOTES_COLUMN:
- self.table.setCurrentIndex(self.table.currentIndex())
- else:
- for itm in (item for item in self.table.selectedItems() if item.column() != col):
- itm.setSelected(False)
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ if col != VALUE_COLUMN:
+ self.table.setCurrentIndex(self.table.currentIndex())
+ else:
+ for itm in (item for item in self.table.selectedItems() if item.column() != col):
+ itm.setSelected(False)
def check_for_deleted_items(self, show_error=False):
- for col_zero_item in (self.table.item(item.row(), self.VALUE_COLUMN) for item in self.table.selectedItems()):
+ for col_zero_item in (self.table.item(item.row(), VALUE_COLUMN) for item in self.table.selectedItems()):
if col_zero_item.is_deleted:
if show_error:
error_dialog(self, _('Selection contains deleted items'),
@@ -708,9 +940,9 @@ class TagListEditor(QDialog, Ui_TagListEditor):
return False
def rename_tag(self):
- if self.table.currentColumn() != self.VALUE_COLUMN:
+ if self.table.currentColumn() != VALUE_COLUMN:
return
- item = self.table.item(self.table.currentRow(), self.VALUE_COLUMN)
+ item = self.table.item(self.table.currentRow(), VALUE_COLUMN)
self._rename_tag(item)
def _rename_tag(self, item):
@@ -723,19 +955,19 @@ class TagListEditor(QDialog, Ui_TagListEditor):
''+_('Items must be undeleted to continue. Do you want '
'to do this?')+'
'):
return
- self.table.blockSignals(True)
- for col_zero_item in (self.table.item(item.row(), self.VALUE_COLUMN) for item in self.table.selectedItems()):
- # undelete any deleted items
- if col_zero_item.is_deleted:
- col_zero_item.set_is_deleted(False)
- self.to_delete.discard(int(col_zero_item.data(Qt.ItemDataRole.UserRole)))
- orig = self.table.item(col_zero_item.row(), 2)
- orig.setData(Qt.ItemDataRole.DisplayRole, '')
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ for col_zero_item in (self.table.item(item.row(), VALUE_COLUMN)
+ for item in self.table.selectedItems()):
+ # undelete any deleted items
+ if col_zero_item.is_deleted:
+ col_zero_item.set_is_deleted(False)
+ self.to_delete.discard(int(col_zero_item.data(Qt.ItemDataRole.UserRole)))
+ orig = self.table.item(col_zero_item.row(), WAS_COLUMN)
+ orig.setData(Qt.ItemDataRole.DisplayRole, '')
self.table.editItem(item)
def delete_pressed(self):
- if self.table.currentColumn() == self.VALUE_COLUMN:
+ if self.table.currentColumn() == VALUE_COLUMN:
self.delete_tags()
return
if not confirm(
@@ -748,7 +980,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
def delete_tags(self):
# This check works because we ensure that the selection is in only one column
- if self.table.currentItem().column() != self.VALUE_COLUMN:
+ if self.table.currentItem().column() != VALUE_COLUMN:
return
# We know the selected items are in column zero
deletes = self.table.selectedItems()
@@ -769,22 +1001,22 @@ class TagListEditor(QDialog, Ui_TagListEditor):
return
row = self.table.row(deletes[0])
- self.table.blockSignals(True)
- for item in deletes:
- id_ = int(item.data(Qt.ItemDataRole.UserRole))
- self.to_delete.add(id_)
- item.set_is_deleted(True)
- row = item.row()
- orig = self.table.item(row, 2)
- orig.setData(Qt.ItemDataRole.DisplayRole, item.initial_text())
- link = self.table.item(row, self.LINK_COLUMN)
- link.setFlags(link.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
- link.setIcon(QIcon.ic('trash.png'))
- self.table.blockSignals(False)
+ with block_signals(self.table):
+ for item in deletes:
+ id_ = int(item.data(Qt.ItemDataRole.UserRole))
+ self.to_delete.add(id_)
+ item.set_is_deleted(True)
+ row = item.row()
+ orig = self.table.item(row, WAS_COLUMN)
+ orig.setData(Qt.ItemDataRole.DisplayRole, item.initial_text())
+ link = self.table.item(row, LINK_COLUMN)
+ link.setFlags(link.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
+ note = self.table.item(row, NOTES_COLUMN)
+ note.setFlags(link.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
if row >= self.table.rowCount():
row = self.table.rowCount() - 1
if row >= 0:
- self.table.scrollToItem(self.table.item(row, self.VALUE_COLUMN))
+ self.table.scrollToItem(self.table.item(row, VALUE_COLUMN))
def record_sort(self, section):
# Note what sort was done so we can redo it when the table is rebuilt
@@ -793,13 +1025,22 @@ class TagListEditor(QDialog, Ui_TagListEditor):
setattr(self, sort_order_attr, 1 - getattr(self, sort_order_attr))
self.last_sorted_by = sort_name
+ def save_geometry(self):
+ gprefs['general_category_editor_row_height'] = self.table.verticalHeader().defaultSectionSize()
+ gprefs['tag_list_editor_table_widths'] = self.table_column_widths
+ super().save_geometry(gprefs, 'tag_list_editor_dialog_geometry')
+
def accepted(self):
- # We don't bother with cleaning out the deleted links because the db
- # interface ignores links for values that don't exist. The caller must
- # process deletes and renames first so the names are correct.
- self.links = {self.table.item(r, self.VALUE_COLUMN).text():self.table.item(r, self.LINK_COLUMN).text()
- for r in range(self.table.rowCount())}
+ for t in self.all_tags.values():
+ if t['is_deleted']:
+ continue
+ if t['key'] in self.to_rename:
+ name = self.to_rename[t['key']]
+ else:
+ name = t['cur_name']
+ self.links[name] = self.current_links.get(t['key'], '')
self.save_geometry()
def rejected(self):
+ self.notes_utilities.restore_all_notes()
self.save_geometry()