When completing names for fields that contain hierarchical data in prefix mode match prefixes after every period. Fixes #2099780 [completion_mode setting](https://bugs.launchpad.net/calibre/+bug/2099780)

This commit is contained in:
Kovid Goyal 2025-02-26 10:30:36 +05:30
parent 3e35e2db76
commit eb1e62a047
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
9 changed files with 74 additions and 9 deletions

View File

@ -6,6 +6,7 @@ __copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from contextlib import suppress
from functools import partial
from qt.core import (
QAbstractItemView,
@ -38,6 +39,10 @@ def containsq(x, prefix):
return primary_contains(prefix, x)
def hierarchy_startswith(x, prefix, sep='.'):
return primary_startswith(x, prefix) or primary_contains(sep + prefix, x)
class CompleteModel(QAbstractListModel): # {{{
def __init__(self, parent=None, sort_func=sort_key, strip_completion_entries=True):
@ -58,7 +63,7 @@ class CompleteModel(QAbstractListModel): # {{{
self.current_prefix = ''
self.endResetModel()
def set_completion_prefix(self, prefix):
def set_completion_prefix(self, prefix, hierarchy_separator: str = ''):
old_prefix = self.current_prefix
self.current_prefix = prefix
if prefix == old_prefix:
@ -71,6 +76,8 @@ class CompleteModel(QAbstractListModel): # {{{
subset = prefix.startswith(old_prefix)
universe = self.current_items if subset else self.all_items
func = primary_startswith if tweaks['completion_mode'] == 'prefix' else containsq
if func is primary_startswith and hierarchy_separator:
func = partial(hierarchy_startswith, sep=hierarchy_separator)
self.beginResetModel()
self.current_items = tuple(x for x in universe if func(x, prefix))
self.endResetModel()
@ -143,8 +150,8 @@ class Completer(QListView): # {{{
if self.isVisible():
self.relayout_needed.emit()
def set_completion_prefix(self, prefix):
self.model().set_completion_prefix(prefix)
def set_completion_prefix(self, prefix, hierarchy_separator: str = ''):
self.model().set_completion_prefix(prefix, hierarchy_separator=hierarchy_separator)
if self.isVisible():
self.relayout_needed.emit()
@ -327,6 +334,7 @@ class LineEdit(QLineEdit, LineEditECM):
self.sep = ','
self.space_before_sep = False
self.add_separator = True
self.hierarchy_separator = ''
self.original_cursor_pos = None
completer_widget = (self if completer_widget is None else
completer_widget)
@ -351,6 +359,9 @@ class LineEdit(QLineEdit, LineEditECM):
def set_separator(self, sep):
self.sep = sep
def set_hierarchy_separator(self, sep: str = '') -> None:
self.hierarchy_separator = sep
def set_space_before_sep(self, space_before):
self.space_before_sep = space_before
@ -392,7 +403,7 @@ class LineEdit(QLineEdit, LineEditECM):
orig = None
if show_all:
orig = self.mcompleter.model().current_prefix
self.mcompleter.set_completion_prefix('')
self.mcompleter.set_completion_prefix('', self.hierarchy_separator)
if not self.mcompleter.model().current_items:
self.mcompleter.hide()
return
@ -421,7 +432,7 @@ class LineEdit(QLineEdit, LineEditECM):
complete_prefix = prefix.lstrip()
if self.sep:
complete_prefix = prefix.split(self.sep)[-1].lstrip()
self.mcompleter.set_completion_prefix(complete_prefix)
self.mcompleter.set_completion_prefix(complete_prefix, self.hierarchy_separator)
def get_completed_text(self, text):
'Get completed text in before and after parts'
@ -500,6 +511,9 @@ class EditWithComplete(EnComboBox):
def set_separator(self, sep):
self.lineEdit().set_separator(sep)
def set_hierarchy_separator(self, sep):
self.lineEdit().set_hierarchy_separator(sep)
def set_space_before_sep(self, space_before):
self.lineEdit().set_space_before_sep(space_before)

View File

@ -66,6 +66,11 @@ class MetadataWidget(Widget, Ui_Form):
self.comment.hide_toolbars()
self.cover.cover_changed.connect(self.change_cover)
self.series.currentTextChanged.connect(self.series_changed)
cuh = self.db.new_api.pref('categories_using_hierarchy', default=())
if 'series' in cuh:
self.series.set_hierarchy_separator('.')
if 'tags' in cuh:
self.tags.set_hierarchy_separator('.')
self.cover.draw_border = False
def change_cover(self, data):

View File

@ -101,6 +101,14 @@ class Base:
except:
pass
@property
def field_name(self) -> str:
return self.db.field_metadata.label_to_key(self.col_metadata['label'], prefer_custom=True)
@property
def hierarchy_separator(self) -> str:
return '.' if self.field_name in self.db.new_api.pref('categories_using_hierarchy', default=()) else ''
def finish_ui_setup(self, parent, edit_widget):
self.was_none = False
w = QWidget(parent)
@ -568,6 +576,10 @@ class MultipleWidget(QWidget):
def set_separator(self, sep):
self.edit_widget.set_separator(sep)
def set_hierarchy_separator(self, sep):
if hasattr(self.edit_widget, 'set_hierarchy_separator'):
self.edit_widget.set_hierarchy_separator(sep)
def set_add_separator(self, sep):
self.edit_widget.set_add_separator(sep)
@ -611,6 +623,7 @@ class Text(Base):
w = MultipleWidget(parent, only_manage_items=True, name=self.col_metadata['name'])
w.set_separator(None)
w.get_editor_button().clicked.connect(super().edit)
w.set_hierarchy_separator(self.hierarchy_separator)
w.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.set_to_undefined = w.clear
self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
@ -693,6 +706,7 @@ class Series(Base):
w.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.set_to_undefined = w.clear
w.set_separator(None)
w.set_hierarchy_separator(self.hierarchy_separator)
self.name_widget = w.edit_widget
self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
self.finish_ui_setup(parent, lambda parent: w)
@ -784,6 +798,7 @@ class Enumeration(Base):
self.key = self.db.field_metadata.label_to_key(self.col_metadata['label'],
prefer_custom=True)
w = MultipleWidget(parent, only_manage_items=True, widget=QComboBox, name=self.col_metadata['name'])
w.set_hierarchy_separator(self.hierarchy_separator)
w.get_editor_button().clicked.connect(self.edit)
w.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
self.widgets = [QLabel(label_string(self.col_metadata['name']), parent)]
@ -1085,6 +1100,8 @@ class BulkBase(Base):
l.setContentsMargins(0, 0, 0, 0)
w.setLayout(l)
self.main_widget = main_widget_class(w)
if (hs := self.hierarchy_separator) and hasattr(self.main_widget, 'set_hierarchy_separator'):
self.main_widget.set_hierarchy_separator(hs)
l.addWidget(self.main_widget)
l.setStretchFactor(self.main_widget, 10)
self.a_c_checkbox = QCheckBox(_('Apply changes'), w)

View File

@ -624,6 +624,12 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.adddate.setSpecialValueText(_('Undefined'))
self.clear_adddate_button.clicked.connect(self.clear_adddate)
self.adddate.dateTimeChanged.connect(self.do_apply_adddate)
cuh = self.db.new_api.pref('categories_using_hierarchy', default=())
if 'series' in cuh:
self.series.set_hierarchy_separator('.')
if 'tags' in cuh:
self.tags.set_hierarchy_separator('.')
self.remove_tags.set_hierarchy_separator('.')
self.casing_algorithm.addItems([
_('Title case'), _('Capitalize'), _('Upper case'), _('Lower case'), _('Swap case')
])

View File

@ -353,6 +353,10 @@ class TextDelegate(StyledItemDelegate, UpdateEditorGeometry, EditableTextDelegat
editor = EditWithComplete(parent)
editor.set_separator(None)
editor.set_clear_button_enabled(False)
if self.auto_complete_function_name.startswith('all_'):
field = self.auto_complete_function_name[4:]
if field in db.new_api.pref('categories_using_hierarchy', default=()):
editor.set_hierarchy_separator('.')
complete_items = [i[1] for i in f()]
editor.update_items_cache(complete_items)
else:
@ -403,6 +407,8 @@ class CompleteDelegate(StyledItemDelegate, UpdateEditorGeometry, EditableTextDel
editor = EditWithComplete(parent)
if col == 'tags':
editor.set_elide_mode(Qt.TextElideMode.ElideMiddle)
if col in db.new_api.pref('categories_using_hierarchy', default=()):
editor.set_hierarchy_separator('.')
editor.set_separator(self.sep)
editor.set_clear_button_enabled(False)
editor.set_space_before_sep(self.space_before_sep)
@ -518,6 +524,8 @@ class CcTextDelegate(StyledItemDelegate, UpdateEditorGeometry, EditableTextDeleg
editor = EditWithComplete(parent)
editor.set_separator(None)
editor.set_clear_button_enabled(False)
if col in m.db.new_api.pref('categories_using_hierarchy', default=()):
editor.set_hierarchy_separator('.')
complete_items = sorted(m.db.all_custom(label=key), key=sort_key)
editor.update_items_cache(complete_items)
else:

View File

@ -660,6 +660,8 @@ class SeriesEdit(EditWithComplete, ToMetadataMixin):
def initialize(self, db, id_):
self.books_to_refresh = set()
if 'series' in db.new_api.pref('categories_using_hierarchy', default=()):
self.set_hierarchy_separator('.')
self.update_items_cache(db.new_api.all_field_names('series'))
series = db.new_api.field_for('series', id_)
self.current_val = self.original_val = series or ''
@ -1502,6 +1504,8 @@ class TagsEdit(EditWithComplete, ToMetadataMixin): # {{{
def initialize(self, db, id_):
self.books_to_refresh = set()
if 'tags' in db.new_api.pref('categories_using_hierarchy', default=()):
self.set_hierarchy_separator('.')
tags = db.tags(id_, index_is_id=True)
tags = tags.split(',') if tags else []
self.current_val = tags

View File

@ -232,6 +232,7 @@ def get_library_init_data(ctx, rd, db, num, sorts, orders, vl):
ans['fts_enabled'] = db.is_fts_enabled()
ans['book_details_vertical_categories'] = db._pref('book_details_vertical_categories', ())
ans['fields_that_support_notes'] = tuple(db._field_supports_notes())
ans['categories_using_hierarchy'] = db._pref('categories_using_hierarchy', ())
mdata = ans['metadata'] = {}
try:
extra_books = {

View File

@ -282,8 +282,13 @@ def query_contains(haystack, needle):
return haystack.toLowerCase().indexOf(needle) is not -1
def query_startswitch(haystack, needle):
return haystack.toLowerCase().indexOf(needle) is 0
def query_startswith(haystack, needle):
return haystack.toLowerCase().startsWith(needle)
def query_startswith_hierarchical(haystack, needle):
haystack = haystack.toLowerCase()
return haystack.startsWith(needle) or haystack.indexOf('.' + needle) is not -1
def update_removals(container_id):
@ -337,7 +342,12 @@ def update_completions(container_id, ok, field, names):
if needle:
interface_data = get_interface_data()
universe = update_completions.names if update_completions.prefix and needle.startswith(update_completions.prefix.toLowerCase()) else names
q = query_contains if interface_data.completion_mode is 'contains' else query_startswitch
if interface_data.completion_mode is 'contains':
q = query_contains
else:
q = query_startswith
if library_data.categories_using_hierarchy and library_data.categories_using_hierarchy.indexOf(field) is not -1:
q = query_startswith_hierarchical
matching_names = [x for x in universe if q(x, needle) and x is not prefix]
else:
matching_names = []

View File

@ -83,7 +83,7 @@ def update_library_data(data):
if library_data.for_library is not current_library_id():
library_data.field_names = {}
library_data.for_library = current_library_id()
for key in 'search_result sortable_fields field_metadata metadata virtual_libraries book_display_fields bools_are_tristate book_details_vertical_categories fts_enabled fields_that_support_notes'.split(' '):
for key in 'search_result sortable_fields field_metadata metadata virtual_libraries book_display_fields bools_are_tristate book_details_vertical_categories fts_enabled fields_that_support_notes categories_using_hierarchy'.split(' '):
library_data[key] = data[key]
sr = library_data.search_result
if sr: