diff --git a/src/calibre/gui2/complete2.py b/src/calibre/gui2/complete2.py index cf7492ebba..7bf4386db9 100644 --- a/src/calibre/gui2/complete2.py +++ b/src/calibre/gui2/complete2.py @@ -6,6 +6,7 @@ __copyright__ = '2012, Kovid Goyal ' __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) diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index d015b32283..b8bb142fc1 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -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): diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 150e07c885..48cfe386e9 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -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) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 9a84be92d5..35b57d8a35 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -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') ]) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index be49192fda..f040428616 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -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: diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index fdbb2c9f42..5b415af265 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -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 diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py index fca4331912..a706b3b4cf 100644 --- a/src/calibre/srv/code.py +++ b/src/calibre/srv/code.py @@ -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 = { diff --git a/src/pyj/book_list/edit_metadata.pyj b/src/pyj/book_list/edit_metadata.pyj index f2ceb8ce78..f18227bb60 100644 --- a/src/pyj/book_list/edit_metadata.pyj +++ b/src/pyj/book_list/edit_metadata.pyj @@ -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 = [] diff --git a/src/pyj/book_list/library_data.pyj b/src/pyj/book_list/library_data.pyj index d3ff737297..d9a8ad41e1 100644 --- a/src/pyj/book_list/library_data.pyj +++ b/src/pyj/book_list/library_data.pyj @@ -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: