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.
This commit is contained in:
Charles Haley 2023-11-01 17:11:59 +00:00 committed by Kovid Goyal
parent 13d584cfb8
commit 3eb15353ec
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 562 additions and 325 deletions

View File

@ -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

View File

@ -2,14 +2,18 @@
# License: GPLv3 Copyright: 2008, Kovid Goyal <kovid at kovidgoyal.net>
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(
'<p>' +
_("This is not one of this column's permitted values ({0})"
).format(', '.join(self.enum_permitted_values)) + '</p>')
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(
'<p>' +
_("This is not one of this column's permitted values ({0})"
).format(', '.join(self.enum_permitted_values)) + '</p>')
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)) +
'</p>', 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):
'<p>'+_('Items must be undeleted to continue. Do you want '
'to do this?')+'<br>'):
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()