Fixes #2016711 [Minor bug: Long-text columns missing from grouped search dropdown](https://bugs.launchpad.net/calibre/+bug/2016711)
This commit is contained in:
Kovid Goyal 2023-04-18 22:00:18 +05:30
commit dd2352bf0e
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 122 additions and 75 deletions

View File

@ -111,20 +111,21 @@ class EditColumnDelegate(QItemDelegate):
editing_finished = pyqtSignal(int) editing_finished = pyqtSignal(int)
editing_started = pyqtSignal(int) editing_started = pyqtSignal(int)
def __init__(self, table): def __init__(self, table, check_for_deleted_items):
QItemDelegate.__init__(self) QItemDelegate.__init__(self)
self.table = table self.table = table
self.completion_data = None self.completion_data = None
self.check_for_deleted_items = check_for_deleted_items
def set_completion_data(self, data): def set_completion_data(self, data):
self.completion_data = data self.completion_data = data
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
self.editing_started.emit(index.row())
if index.column() == 0: if index.column() == 0:
self.item = self.table.itemFromIndex(index) if self.check_for_deleted_items(show_error=True):
if self.item.is_deleted:
return None return None
self.editing_started.emit(index.row())
self.item = self.table.itemFromIndex(index)
if self.completion_data: if self.completion_data:
editor = EditWithComplete(parent) editor = EditWithComplete(parent)
editor.set_separator(None) editor.set_separator(None)
@ -132,6 +133,7 @@ class EditColumnDelegate(QItemDelegate):
else: else:
editor = EnLineEdit(parent) editor = EnLineEdit(parent)
return editor return editor
self.editing_started.emit(index.row())
editor = EnLineEdit(parent) editor = EnLineEdit(parent)
editor.setClearButtonEnabled(True) editor.setClearButtonEnabled(True)
return editor return editor
@ -185,19 +187,19 @@ class TagListEditor(QDialog, Ui_TagListEditor):
hh = self.table.horizontalHeader() hh = self.table.horizontalHeader()
hh.sectionResized.connect(self.table_column_resized) hh.sectionResized.connect(self.table_column_resized)
hh.setSectionsClickable(True) hh.setSectionsClickable(True)
hh.sectionClicked.connect(self.do_sort) self.table.setSortingEnabled(True)
hh.sectionClicked.connect(self.record_sort)
hh.setSortIndicatorShown(True) hh.setSortIndicatorShown(True)
self.sort_names = ('name', 'count', 'was', 'link')
self.last_sorted_by = 'name' self.last_sorted_by = 'name'
self.name_order = 0 self.name_order = self.count_order = self.was_order = self.link_order = 1
self.count_order = 1
self.was_order = 1
self.link_order = 0
self.edit_delegate = EditColumnDelegate(self.table) 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(0, self.edit_delegate)
self.table.setItemDelegateForColumn(3, self.edit_delegate)
if prefs['case_sensitive']: if prefs['case_sensitive']:
self.string_contains = contains self.string_contains = contains
@ -210,12 +212,16 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.undo_button.clicked.connect(self.undo_edit) self.undo_button.clicked.connect(self.undo_edit)
self.table.itemDoubleClicked.connect(self._rename_tag) self.table.itemDoubleClicked.connect(self._rename_tag)
self.table.itemChanged.connect(self.finish_editing) self.table.itemChanged.connect(self.finish_editing)
self.table.itemSelectionChanged.connect(self.selection_changed)
self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(_('&OK'))
self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(_('&Cancel')) self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(_('&Cancel'))
self.buttonBox.accepted.connect(self.accepted) self.buttonBox.accepted.connect(self.accepted)
self.buttonBox.rejected.connect(self.rejected) self.buttonBox.rejected.connect(self.rejected)
# Ensure that the selection moves with the item focus
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems)
self.search_box.initialize('tag_list_search_box_' + cat_name) self.search_box.initialize('tag_list_search_box_' + cat_name)
le = self.search_box.lineEdit() le = self.search_box.lineEdit()
ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION) ac = le.findChild(QAction, QT_HIDDEN_CLEAR_ACTION)
@ -432,7 +438,8 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.setRowCount(len(tags)) self.table.setRowCount(len(tags))
for row,tag in enumerate(tags): for row,tag in enumerate(tags):
item = NameTableWidgetItem(self.sorter) item = NameTableWidgetItem(self.sorter)
item.set_is_deleted(self.all_tags[tag]['is_deleted']) is_deleted = self.all_tags[tag]['is_deleted']
item.set_is_deleted(is_deleted)
_id = self.all_tags[tag]['key'] _id = self.all_tags[tag]['key']
item.setData(Qt.ItemDataRole.UserRole, _id) item.setData(Qt.ItemDataRole.UserRole, _id)
item.set_initial_text(tag) item.set_initial_text(tag)
@ -470,19 +477,19 @@ 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.setText(_('no links available')) item.setText(_('no links available'))
else: else:
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable) if is_deleted:
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) 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, '')) item.setText(self.link_map.get(tag, ''))
self.table.setItem(row, 3, item) self.table.setItem(row, 3, item)
if self.last_sorted_by == 'name': # re-sort the table
self.table.sortByColumn(0, Qt.SortOrder(self.name_order)) column = self.sort_names.index(self.last_sorted_by)
elif self.last_sorted_by == 'count': sort_order = getattr(self, self.last_sorted_by + '_order')
self.table.sortByColumn(1, Qt.SortOrder(self.count_order)) self.table.sortByColumn(column, Qt.SortOrder(sort_order))
elif self.last_sorted_by == 'link':
self.table.sortByColumn(3, Qt.SortOrder(self.link_order))
else:
self.table.sortByColumn(2, Qt.SortOrder(self.was_order))
if select_item is not None: if select_item is not None:
self.table.setCurrentItem(select_item) self.table.setCurrentItem(select_item)
@ -532,6 +539,11 @@ class TagListEditor(QDialog, Ui_TagListEditor):
super().save_geometry(gprefs, 'tag_list_editor_dialog_geometry') super().save_geometry(gprefs, 'tag_list_editor_dialog_geometry')
def start_editing(self, on_row): 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 != 0:
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)
for item in items: for item in items:
@ -542,6 +554,8 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.blockSignals(False) self.table.blockSignals(False)
def stop_editing(self, on_row): 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.
items = self.table.selectedItems() items = self.table.selectedItems()
self.table.blockSignals(True) self.table.blockSignals(True)
for item in items: for item in items:
@ -551,6 +565,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
def finish_editing(self, edited_item): def finish_editing(self, edited_item):
if edited_item.column() != 0: if edited_item.column() != 0:
# Nothing to do for link fields
return return
if not edited_item.text(): if not edited_item.text():
error_dialog(self, _('Item is blank'), _( error_dialog(self, _('Item is blank'), _(
@ -581,8 +596,8 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.blockSignals(False) self.table.blockSignals(False)
def undo_edit(self): def undo_edit(self):
indexes = self.table.selectionModel().selectedRows() col_zero_items = (self.table.item(item.row(), 0) for item in self.table.selectedItems())
if not indexes: 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()
return return
@ -592,17 +607,45 @@ class TagListEditor(QDialog, Ui_TagListEditor):
'tag_list_editor_undo'): 'tag_list_editor_undo'):
return return
self.table.blockSignals(True) self.table.blockSignals(True)
for idx in indexes: for col_zero_item in col_zero_items:
row = idx.row() col_zero_item.setText(col_zero_item.initial_text())
item = self.table.item(row, 0) col_zero_item.set_is_deleted(False)
item.setText(item.initial_text()) self.to_delete.discard(int(col_zero_item.data(Qt.ItemDataRole.UserRole)))
item.set_is_deleted(False) self.to_rename.pop(int(col_zero_item.data(Qt.ItemDataRole.UserRole)), None)
self.to_delete.discard(int(item.data(Qt.ItemDataRole.UserRole))) row = col_zero_item.row()
self.to_rename.pop(int(item.data(Qt.ItemDataRole.UserRole)), None)
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.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsSelectable)
item.setIcon(QIcon())
self.table.blockSignals(False) self.table.blockSignals(False)
def selection_changed(self):
col0 = tuple(item for item in self.table.selectedItems() if item.column() == 0)
col3 = tuple(item for item in self.table.selectedItems() if item.column() == 3)
if col0 and col3:
error_dialog(self, _('Cannot select in multiple columns'),
'<p>'+_('Selection of items in multiple columns is not supported. '
'The selection will be cleared')+'<br>',
show=True)
sm = self.table.selectionModel()
self.table.blockSignals(True)
sm.clear()
self.table.blockSignals(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()):
if col_zero_item.is_deleted:
if show_error:
error_dialog(self, _('Selection contains deleted items'),
'<p>'+_('The selection contains deleted items. You '
'must undelete them before editing.')+'<br>',
show=True)
return True
return False
def rename_tag(self): def rename_tag(self):
if self.table.currentColumn() != 0:
return
item = self.table.item(self.table.currentRow(), 0) item = self.table.item(self.table.currentRow(), 0)
self._rename_tag(item) self._rename_tag(item)
@ -611,14 +654,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
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()
return return
for col_zero_item in self.table.selectedItems(): if self.check_for_deleted_items():
if col_zero_item.is_deleted: if not question_dialog(self, _('Undelete items?'),
if not question_dialog(self, _('Undelete items?'), '<p>'+_('Items must be undeleted to continue. Do you want '
'<p>'+_('Items must be undeleted to continue. Do you want ' '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.selectedItems(): for col_zero_item in (self.table.item(item.row(), 0) 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)
@ -631,14 +673,25 @@ class TagListEditor(QDialog, Ui_TagListEditor):
def delete_pressed(self): def delete_pressed(self):
if self.table.currentColumn() == 0: if self.table.currentColumn() == 0:
self.delete_tags() self.delete_tags()
return
if not confirm(
'<p>'+_('Are you sure you want to delete the selected links? '
'There is no undo.')+'<br>',
'tag_list_editor_link_delete'):
return
for item in self.table.selectedItems():
item.setText('')
def delete_tags(self): def delete_tags(self):
# This check works because we ensure that the selection is in only one column
if self.table.currentItem().column() != 0:
return
# We know the selected items are in column zero
deletes = self.table.selectedItems() deletes = self.table.selectedItems()
if not deletes: if not deletes:
error_dialog(self, _('No items selected'), error_dialog(self, _('No items selected'),
_('You must select at least one item from the list.')).exec() _('You must select at least one item from the list.')).exec()
return return
to_del = [] to_del = []
for item in deletes: for item in deletes:
if not item.is_deleted: if not item.is_deleted:
@ -657,39 +710,31 @@ class TagListEditor(QDialog, Ui_TagListEditor):
id_ = int(item.data(Qt.ItemDataRole.UserRole)) id_ = int(item.data(Qt.ItemDataRole.UserRole))
self.to_delete.add(id_) self.to_delete.add(id_)
item.set_is_deleted(True) item.set_is_deleted(True)
orig = self.table.item(item.row(), 2) row = item.row()
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.setFlags(link.flags() & ~(Qt.ItemFlag.ItemIsSelectable|Qt.ItemFlag.ItemIsEditable))
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, 0))
def do_sort(self, section): def record_sort(self, section):
(self.do_sort_by_name, self.do_sort_by_count, self.do_sort_by_was, self.do_sort_by_link)[section]() # Note what sort was done so we can redo it when the table is rebuilt
sort_name = self.sort_names[section]
def do_sort_by_name(self): sort_order_attr = sort_name + '_order'
self.name_order = 1 - self.name_order setattr(self, sort_order_attr, 1 - getattr(self, sort_order_attr))
self.last_sorted_by = 'name' self.last_sorted_by = sort_name
self.table.sortByColumn(0, Qt.SortOrder(self.name_order))
def do_sort_by_count(self):
self.count_order = 1 - self.count_order
self.last_sorted_by = 'count'
self.table.sortByColumn(1, Qt.SortOrder(self.count_order))
def do_sort_by_was(self):
self.was_order = 1 - self.was_order
self.last_sorted_by = 'count'
self.table.sortByColumn(2, Qt.SortOrder(self.was_order))
def do_sort_by_link(self):
self.link_order = 1 - self.link_order
self.last_sorted_by = 'link'
self.table.sortByColumn(3, Qt.SortOrder(self.link_order))
def accepted(self): def accepted(self):
self.links = {self.table.item(r, 0).text():self.table.item(r, 3).text() for r in range(self.table.rowCount())} # 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, 0).text():self.table.item(r, 3).text()
for r in range(self.table.rowCount())}
self.save_geometry() self.save_geometry()
def rejected(self): def rejected(self):

View File

@ -55,26 +55,28 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
"grouped search term in the drop-down box, enter the list of columns " "grouped search term in the drop-down box, enter the list of columns "
"to search in the value box, then push the Save button. " "to search in the value box, then push the Save button. "
"<p>Note: Search terms are forced to lower case; <code>MySearch</code> " "<p>Note: Search terms are forced to lower case; <code>MySearch</code> "
"and <code>mysearch</code> are the same term." "and <code>mysearch</code> are the same term. Search terms cannot be "
"<p>You can have your grouped search term show up as User categories in " "hierarchical. Periods are not allowed in the term name."
" the Tag browser. Just add the grouped search term names to the Make User " "<p>Grouped search terms can show as User categories in the Tag browser "
"categories from box. You can add multiple terms separated by commas. " "by adding the grouped search term names to the 'Make User "
"The new User category will be automatically " "categories from' box. Multiple terms are separated by commas. "
"populated with all the items in the categories included in the grouped " "These 'automatic user categories' will be populated with items "
"search term. <p>Automatic User categories permit you to see easily " "from the categories included in the grouped search term. "
"all the category items that " "<p>Automatic user categories permit you to see all the category items that "
"are in the columns contained in the grouped search term. Using the above " "are in the columns contained in the grouped search term. Using the above "
"<code>allseries</code> example, the automatically-generated User category " "<code>allseries</code> example, the automatic user category "
"will contain all the series mentioned in <code>series</code>, " "will contain all the series names in <code>series</code>, "
"<code>#myseries</code>, and <code>#myseries2</code>. This " "<code>#myseries</code>, and <code>#myseries2</code>. This "
"can be useful to check for duplicates, to find which column contains " "can be useful to check for duplicates, to find which column contains "
"a particular item, or to have hierarchical categories (categories " "a particular item, or to have hierarchical categories (categories "
"that contain categories).")) "that contain categories). "
"<p>Note: values from non-category columns such as comments won't appear "
"in automatic user categories. "))
self.gst = db.prefs.get('grouped_search_terms', {}).copy() self.gst = db.prefs.get('grouped_search_terms', {}).copy()
self.orig_gst_keys = list(self.gst.keys()) self.orig_gst_keys = list(self.gst.keys())
fm = db.new_api.field_metadata fm = db.new_api.field_metadata
categories = [x[0] for x in find_categories(fm) if fm[x[0]]['search_terms']] categories = [x for x in fm.keys() if not x.startswith('@') and fm[x]['search_terms']]
self.gst_value.update_items_cache(categories) self.gst_value.update_items_cache(categories)
QTimer.singleShot(0, self.fill_gst_box) QTimer.singleShot(0, self.fill_gst_box)

View File

@ -306,7 +306,7 @@ class TagBrowserMixin: # {{{
db.new_api.remove_items(category, to_delete) db.new_api.remove_items(category, to_delete)
db.new_api.rename_items(category, to_rename, change_index=False) db.new_api.rename_items(category, to_rename, change_index=False)
# Must do this at the end so renames are accounted for # Must do this at the end so renames and deletes are accounted for
db.new_api.set_link_map(category, d.links) db.new_api.set_link_map(category, d.links)
# Clean up the library view # Clean up the library view