Add editing notes to manage categories, and to the context menu of the tag browser.

Still to do: adding notes to manage authors.
This commit is contained in:
Charles Haley 2023-09-09 11:38:00 +01:00 committed by Kovid Goyal
parent c11c051309
commit a240ef62ec
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 112 additions and 28 deletions

View File

@ -4,15 +4,16 @@
from functools import partial from functools import partial
from qt.core import ( from qt.core import (
QAbstractItemView, QAction, QApplication, QColor, QDialog, QDialogButtonBox, QFrame, QAbstractItemView, QAction, QApplication, QCheckBox, QColor, QDialog,
QIcon, QItemDelegate, QLabel, QMenu, QSize, Qt, QTableWidgetItem, QTimer, QDialogButtonBox, QFrame, QHBoxLayout, QIcon, QItemDelegate, QLabel, QMenu,
pyqtSignal, sip, QSize, Qt, QTableWidgetItem, QTimer, QToolButton, QWidget, pyqtSignal, sip,
) )
from calibre.gui2 import error_dialog, gprefs, question_dialog from calibre.gui2 import error_dialog, gprefs, question_dialog
from calibre.gui2.actions.show_quickview import get_quickview_action_plugin from calibre.gui2.actions.show_quickview import get_quickview_action_plugin
from calibre.gui2.complete2 import EditWithComplete from calibre.gui2.complete2 import EditWithComplete
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
from calibre.gui2.dialogs.tag_list_editor_table_widget import TleTableWidget from calibre.gui2.dialogs.tag_list_editor_table_widget import TleTableWidget
from calibre.gui2.widgets import EnLineEdit from calibre.gui2.widgets import EnLineEdit
@ -144,8 +145,54 @@ class EditColumnDelegate(QItemDelegate):
QItemDelegate.destroyEditor(self, editor, index) QItemDelegate.destroyEditor(self, editor, index)
class NotesItemWidget(QWidget):
clicked = pyqtSignal(object, object, object, object)
icon = QIcon.ic('edit_input.png')
def __init__(self, db, field, item_id, row):
super().__init__()
self.row = row
self.field = field
self.item_id = item_id
self.db = db
l = QHBoxLayout()
l.setContentsMargins(2, 0, 0, 0)
self.setLayout(l)
cb = self.cb = QCheckBox()
cb.setEnabled(False)
l.addWidget(cb)
tb = self.tb = QToolButton()
tb.setIcon(self.icon)
tb.clicked.connect(self.tb_clicked)
l.addWidget(tb)
l.addStretch(3)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.set_checked()
def keyPressEvent(self, ev):
# Use this instead of focusProxy() because fp() changes selection behavior
if ev.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return, Qt.Key.Key_Space):
ev.accept()
self.tb_clicked()
return
return super().keyPressEvent(ev)
def tb_clicked(self):
self.clicked.emit(self, self.field, self.item_id, self.db)
def set_checked(self):
t = self.db.notes_for(self.field, self.item_id)
self.cb.setChecked(bool(t))
class TagListEditor(QDialog, Ui_TagListEditor): 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, 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): ttm_is_first_letter=False, category=None, fm=None, link_map=None):
QDialog.__init__(self, window) QDialog.__init__(self, window)
@ -228,7 +275,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
def show_context_menu(self, point): def show_context_menu(self, point):
idx = self.table.indexAt(point) idx = self.table.indexAt(point)
if idx.column() != 0: if idx.column() != self.VALUE_COLUMN:
return return
m = self.au_context_menu = QMenu(self) m = self.au_context_menu = QMenu(self)
@ -339,8 +386,8 @@ class TagListEditor(QDialog, Ui_TagListEditor):
return return
for _ in range(0, self.table.rowCount()): for _ in range(0, self.table.rowCount()):
r = self.search_item_row = (self.search_item_row + 1) % 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, 0).text()): if self.string_contains(find_text, self.table.item(r, self.VALUE_COLUMN).text()):
self.table.setCurrentItem(self.table.item(r, 0)) self.table.setCurrentItem(self.table.item(r, self.VALUE_COLUMN))
self.table.setFocus(Qt.FocusReason.OtherFocusReason) self.table.setFocus(Qt.FocusReason.OtherFocusReason)
return return
# Nothing found. Pop up the little dialog for 1.5 seconds # Nothing found. Pop up the little dialog for 1.5 seconds
@ -385,13 +432,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
vh.setDefaultSectionSize(gprefs.get('general_category_editor_row_height', vh.defaultSectionSize())) vh.setDefaultSectionSize(gprefs.get('general_category_editor_row_height', vh.defaultSectionSize()))
vh.sectionResized.connect(self.row_height_changed) vh.sectionResized.connect(self.row_height_changed)
self.table.setColumnCount(4) 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.edit_delegate.editing_finished.connect(self.stop_editing) self.edit_delegate.editing_finished.connect(self.stop_editing)
self.edit_delegate.editing_started.connect(self.start_editing) self.edit_delegate.editing_started.connect(self.start_editing)
self.table.setItemDelegateForColumn(0, self.edit_delegate) self.table.setItemDelegateForColumn(self.VALUE_COLUMN, self.edit_delegate)
self.table.setItemDelegateForColumn(3, self.edit_delegate) self.table.setItemDelegateForColumn(self.LINK_COLUMN, self.edit_delegate)
self.table.delete_pressed.connect(self.delete_pressed) self.table.delete_pressed.connect(self.delete_pressed)
self.table.itemDoubleClicked.connect(self._rename_tag) self.table.itemDoubleClicked.connect(self._rename_tag)
@ -449,13 +496,15 @@ class TagListEditor(QDialog, Ui_TagListEditor):
select_item = None select_item = None
self.table.blockSignals(True) self.table.blockSignals(True)
self.name_col = QTableWidgetItem(self.category_name) self.name_col = QTableWidgetItem(self.category_name)
self.table.setHorizontalHeaderItem(0, self.name_col) self.table.setHorizontalHeaderItem(self.VALUE_COLUMN, self.name_col)
self.count_col = QTableWidgetItem(_('Count')) self.count_col = QTableWidgetItem(_('Count'))
self.table.setHorizontalHeaderItem(1, self.count_col) self.table.setHorizontalHeaderItem(1, self.count_col)
self.was_col = QTableWidgetItem(_('Was')) self.was_col = QTableWidgetItem(_('Was'))
self.table.setHorizontalHeaderItem(2, self.was_col) self.table.setHorizontalHeaderItem(2, self.was_col)
self.link_col = QTableWidgetItem(_('Link')) self.link_col = QTableWidgetItem(_('Link'))
self.table.setHorizontalHeaderItem(3, self.link_col) self.table.setHorizontalHeaderItem(self.LINK_COLUMN, self.link_col)
self.link_col = QTableWidgetItem(_('Notes'))
self.table.setHorizontalHeaderItem(4, self.link_col)
self.table.setRowCount(len(tags)) self.table.setRowCount(len(tags))
for row,tag in enumerate(tags): for row,tag in enumerate(tags):
@ -476,7 +525,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
_("This is not one of this column's permitted values ({0})" _("This is not one of this column's permitted values ({0})"
).format(', '.join(self.enum_permitted_values)) + '</p>') ).format(', '.join(self.enum_permitted_values)) + '</p>')
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable) item.setFlags(item.flags() | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable)
self.table.setItem(row, 0, item) self.table.setItem(row, self.VALUE_COLUMN, item)
if select_item is None: if select_item is None:
if ttm_is_first_letter: if ttm_is_first_letter:
if primary_startswith(tag, tag_to_match): if primary_startswith(tag, tag_to_match):
@ -506,7 +555,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
item.setFlags(item.flags() | (Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable)) item.setFlags(item.flags() | (Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
item.setIcon(QIcon()) item.setIcon(QIcon())
item.setText(self.link_map.get(tag, '')) item.setText(self.link_map.get(tag, ''))
self.table.setItem(row, 3, item) self.table.setItem(row, self.LINK_COLUMN, item)
if self.category is not None:
from calibre.gui2.ui import get_gui
nw = NotesItemWidget(get_gui().current_db.new_api, self.category, _id, row)
nw.clicked.connect(self.notes_button_clicked)
self.table.setCellWidget(row, 4, nw)
# re-sort the table # re-sort the table
column = self.sort_names.index(self.last_sorted_by) column = self.sort_names.index(self.last_sorted_by)
@ -523,6 +578,10 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.start_find_pos = -1 self.start_find_pos = -1
self.table.blockSignals(False) self.table.blockSignals(False)
def notes_button_clicked(self, w, field, item_id, db):
EditNoteDialog(field, item_id, db).exec()
w.set_checked()
def not_found_label_timer_event(self): def not_found_label_timer_event(self):
self.not_found_label.setVisible(False) self.not_found_label.setVisible(False)
@ -565,7 +624,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
current_column = self.table.currentItem().column() current_column = self.table.currentItem().column()
# We don't support editing multiple link rows at the same time. Use # We don't support editing multiple link rows at the same time. Use
# the current cell. # the current cell.
if current_column != 0: if current_column != self.VALUE_COLUMN:
self.table.setCurrentItem(self.table.item(on_row, current_column)) self.table.setCurrentItem(self.table.item(on_row, current_column))
items = self.table.selectedItems() items = self.table.selectedItems()
self.table.blockSignals(True) self.table.blockSignals(True)
@ -589,7 +648,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.blockSignals(False) self.table.blockSignals(False)
def finish_editing(self, edited_item): def finish_editing(self, edited_item):
if edited_item.column() != 0: if edited_item.column() != self.VALUE_COLUMN:
# Nothing to do for link fields # Nothing to do for link fields
return return
if not edited_item.text(): if not edited_item.text():
@ -621,7 +680,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.blockSignals(False) self.table.blockSignals(False)
def undo_edit(self): def undo_edit(self):
col_zero_items = (self.table.item(item.row(), 0) for item in self.table.selectedItems()) col_zero_items = (self.table.item(item.row(), self.VALUE_COLUMN) for item in self.table.selectedItems())
if not col_zero_items: if not col_zero_items:
error_dialog(self, _('No item selected'), error_dialog(self, _('No item selected'),
_('You must select one item from the list of available items.')).exec() _('You must select one item from the list of available items.')).exec()
@ -639,7 +698,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.to_rename.pop(int(col_zero_item.data(Qt.ItemDataRole.UserRole)), None) self.to_rename.pop(int(col_zero_item.data(Qt.ItemDataRole.UserRole)), None)
row = col_zero_item.row() row = col_zero_item.row()
self.table.item(row, 2).setData(Qt.ItemDataRole.DisplayRole, '') self.table.item(row, 2).setData(Qt.ItemDataRole.DisplayRole, '')
item = self.table.item(row, 3) item = self.table.item(row, self.LINK_COLUMN)
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsSelectable) item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsSelectable)
item.setIcon(QIcon()) item.setIcon(QIcon())
self.table.blockSignals(False) self.table.blockSignals(False)
@ -648,12 +707,15 @@ class TagListEditor(QDialog, Ui_TagListEditor):
if self.table.currentIndex().isValid(): if self.table.currentIndex().isValid():
col = self.table.currentIndex().column() col = self.table.currentIndex().column()
self.table.blockSignals(True) 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): for itm in (item for item in self.table.selectedItems() if item.column() != col):
itm.setSelected(False) itm.setSelected(False)
self.table.blockSignals(False) self.table.blockSignals(False)
def check_for_deleted_items(self, show_error=False): def check_for_deleted_items(self, show_error=False):
for col_zero_item in (self.table.item(item.row(), 0) for item in self.table.selectedItems()): for col_zero_item in (self.table.item(item.row(), self.VALUE_COLUMN) for item in self.table.selectedItems()):
if col_zero_item.is_deleted: if col_zero_item.is_deleted:
if show_error: if show_error:
error_dialog(self, _('Selection contains deleted items'), error_dialog(self, _('Selection contains deleted items'),
@ -664,9 +726,9 @@ class TagListEditor(QDialog, Ui_TagListEditor):
return False return False
def rename_tag(self): def rename_tag(self):
if self.table.currentColumn() != 0: if self.table.currentColumn() != self.VALUE_COLUMN:
return return
item = self.table.item(self.table.currentRow(), 0) item = self.table.item(self.table.currentRow(), self.VALUE_COLUMN)
self._rename_tag(item) self._rename_tag(item)
def _rename_tag(self, item): def _rename_tag(self, item):
@ -680,7 +742,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
'to do this?')+'<br>'): 'to do this?')+'<br>'):
return return
self.table.blockSignals(True) self.table.blockSignals(True)
for col_zero_item in (self.table.item(item.row(), 0) for item in self.table.selectedItems()): for col_zero_item in (self.table.item(item.row(), self.VALUE_COLUMN) for item in self.table.selectedItems()):
# undelete any deleted items # undelete any deleted items
if col_zero_item.is_deleted: if col_zero_item.is_deleted:
col_zero_item.set_is_deleted(False) col_zero_item.set_is_deleted(False)
@ -691,7 +753,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.editItem(item) self.table.editItem(item)
def delete_pressed(self): def delete_pressed(self):
if self.table.currentColumn() == 0: if self.table.currentColumn() == self.VALUE_COLUMN:
self.delete_tags() self.delete_tags()
return return
if not confirm( if not confirm(
@ -704,7 +766,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
def delete_tags(self): def delete_tags(self):
# This check works because we ensure that the selection is in only one column # This check works because we ensure that the selection is in only one column
if self.table.currentItem().column() != 0: if self.table.currentItem().column() != self.VALUE_COLUMN:
return return
# We know the selected items are in column zero # We know the selected items are in column zero
deletes = self.table.selectedItems() deletes = self.table.selectedItems()
@ -733,14 +795,14 @@ class TagListEditor(QDialog, Ui_TagListEditor):
row = item.row() row = item.row()
orig = self.table.item(row, 2) orig = self.table.item(row, 2)
orig.setData(Qt.ItemDataRole.DisplayRole, item.initial_text()) orig.setData(Qt.ItemDataRole.DisplayRole, item.initial_text())
link = self.table.item(row, 3) link = self.table.item(row, self.LINK_COLUMN)
link.setFlags(link.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable)) link.setFlags(link.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
link.setIcon(QIcon.ic('trash.png')) link.setIcon(QIcon.ic('trash.png'))
self.table.blockSignals(False) self.table.blockSignals(False)
if row >= self.table.rowCount(): if row >= self.table.rowCount():
row = self.table.rowCount() - 1 row = self.table.rowCount() - 1
if row >= 0: if row >= 0:
self.table.scrollToItem(self.table.item(row, 0)) self.table.scrollToItem(self.table.item(row, self.VALUE_COLUMN))
def record_sort(self, section): def record_sort(self, section):
# Note what sort was done so we can redo it when the table is rebuilt # Note what sort was done so we can redo it when the table is rebuilt
@ -753,7 +815,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
# We don't bother with cleaning out the deleted links because the db # 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 # interface ignores links for values that don't exist. The caller must
# process deletes and renames first so the names are correct. # process deletes and renames first so the names are correct.
self.links = {self.table.item(r, 0).text():self.table.item(r, 3).text() 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 r in range(self.table.rowCount())}
self.save_geometry() self.save_geometry()

View File

@ -127,6 +127,17 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="4">
<widget class="QLabel">
<property name="text">
<string>Notes changes are immediate</string>
</property>
<property name="toolTip">
<string>Creating or changing a note has immediate effect. They are not
reverted by undo or canceling this dialog</string>
</property>
</widget>
</item>
<item row="2" column="0"> <item row="2" column="0">
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item> <item>

View File

@ -24,6 +24,7 @@ from calibre.gui2 import (
FunctionDispatcher, choose_files, config, empty_index, gprefs, pixmap_to_data, FunctionDispatcher, choose_files, config, empty_index, gprefs, pixmap_to_data,
question_dialog, rating_font, question_dialog, rating_font,
) )
from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog
from calibre.gui2.complete2 import EditWithComplete from calibre.gui2.complete2 import EditWithComplete
from calibre.gui2.tag_browser.model import ( from calibre.gui2.tag_browser.model import (
COUNT_ROLE, DRAG_IMAGE_ROLE, TAG_SEARCH_STATES, TagsModel, TagTreeItem, COUNT_ROLE, DRAG_IMAGE_ROLE, TAG_SEARCH_STATES, TagsModel, TagTreeItem,
@ -522,6 +523,9 @@ class TagsView(QTreeView): # {{{
return return
from calibre.gui2.ui import get_gui from calibre.gui2.ui import get_gui
try: try:
if action == 'edit_note':
EditNoteDialog(category, extra, self.db).exec()
return
if action == 'dont_collapse_category': if action == 'dont_collapse_category':
if key not in extra: if key not in extra:
extra.append(key) extra.append(key)
@ -844,6 +848,13 @@ class TagsView(QTreeView): # {{{
_('Remove %s from selected books') % display_name(tag), _('Remove %s from selected books') % display_name(tag),
partial(self.context_menu_handler, action='remove_tag', index=index)) partial(self.context_menu_handler, action='remove_tag', index=index))
item_id = self.db.new_api.get_item_id(tag.category, tag.original_name)
has_note = bool(self.db.new_api.notes_for(tag.category, item_id))
self.context_menu.addAction(self.edit_metadata_icon,
(_('Edit note for %s') if has_note else _('Create note for %s'))%display_name(tag),
partial(self.context_menu_handler, action='edit_note',
index=index, extra=item_id, category=tag.category))
elif key == 'search' and tag.is_searchable: elif key == 'search' and tag.is_searchable:
self.context_menu.addAction(self.rename_icon, self.context_menu.addAction(self.rename_icon,
_('Rename %s')%display_name(tag), _('Rename %s')%display_name(tag),