From ff1952b8b785770289b918462dfb408fe10b38dc Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 8 Jan 2023 11:26:46 +0000 Subject: [PATCH] Bug #2002195: Search breaks if non-number used in numeric-sorted column. This fix is really an enhancement, adding better error presentation to the GUI search box. --- src/calibre/db/search.py | 55 +++++++++++++++++----------------- src/calibre/gui2/layout.py | 5 ++++ src/calibre/gui2/search_box.py | 11 +++++++ 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/calibre/db/search.py b/src/calibre/db/search.py index afbbe7de1e..efb507c4d7 100644 --- a/src/calibre/db/search.py +++ b/src/calibre/db/search.py @@ -56,36 +56,36 @@ def _match(query, value, matchkind, use_primary_find_in_search=True, case_sensit else: internal_match_ok = False for t in value: - try: # ignore regexp exceptions, required because search-ahead tries before typing is finished - if not case_sensitive: - t = icu_lower(t) - if (matchkind == EQUALS_MATCH): - if internal_match_ok: - if query == t: - return True - return sq in [c.strip() for c in t.split('.') if c.strip()] - elif query[0] == '.': - if t.startswith(query[1:]): - ql = len(query) - 1 - if (len(t) == ql) or (t[ql:ql+1] == '.'): - return True - elif query == t: + if not case_sensitive: + t = icu_lower(t) + if (matchkind == EQUALS_MATCH): + if internal_match_ok: + if query == t: return True - elif matchkind == REGEXP_MATCH: - flags = regex.UNICODE | regex.VERSION1 | regex.FULLCASE | (0 if case_sensitive else regex.IGNORECASE) + return sq in [c.strip() for c in t.split('.') if c.strip()] + elif query[0] == '.': + if t.startswith(query[1:]): + ql = len(query) - 1 + if (len(t) == ql) or (t[ql:ql+1] == '.'): + return True + elif query == t: + return True + elif matchkind == REGEXP_MATCH: + flags = regex.UNICODE | regex.VERSION1 | regex.FULLCASE | (0 if case_sensitive else regex.IGNORECASE) + try: if regex.search(query, t, flags) is not None: return True - elif matchkind == ACCENT_MATCH: - if primary_contains(query, t): + except regex.error as e: + raise ParseException(_('Invalid regular expression: {}').format(str(e))) + elif matchkind == ACCENT_MATCH: + if primary_contains(query, t): + return True + elif matchkind == CONTAINS_MATCH: + if not case_sensitive and use_primary_find_in_search: + if primary_no_punc_contains(query, t): return True - elif matchkind == CONTAINS_MATCH: - if not case_sensitive and use_primary_find_in_search: - if primary_no_punc_contains(query, t): - return True - elif query in t: - return True - except regex.error: - pass + elif query in t: + return True return False # }}} @@ -298,7 +298,8 @@ class NumericSearch: # {{{ try: v = cast(val) except Exception: - v = None + raise ParseException( + _('Non-numeric value in column {0}: {1}').format(location, val)) if v: v = adjust(v) if relop(v, q): diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index da49012515..51e8145548 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -242,6 +242,11 @@ class SearchBar(QFrame): # {{{ _('Advanced search'), default_keys=("Shift+Ctrl+F",), action=ac) + # This error icon will be placed after the clear button icon + parent.search.parse_error_action = ac = parent.search.add_action('dialog_error.png', QLineEdit.ActionPosition.TrailingPosition) + parent.addAction(ac) + ac.setVisible(False) + self.search_button = QToolButton() self.search_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly) self.search_button.setIcon(QIcon.ic('search.png')) diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 7f26d8c0d0..5a42cd62f7 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -142,6 +142,7 @@ class SearchBox2(QComboBox): # {{{ self.setMinimumContentsLength(25) self._in_a_search = False self.tool_tip_text = self.toolTip() + self.parse_error_action = None def add_action(self, icon, position=QLineEdit.ActionPosition.TrailingPosition): if not isinstance(icon, QIcon): @@ -180,6 +181,7 @@ class SearchBox2(QComboBox): # {{{ return self.currentText() def clear(self, emit_search=True): + self.show_parse_error_action(False) self.normalize_state() self.setEditText('') if emit_search: @@ -191,9 +193,17 @@ class SearchBox2(QComboBox): # {{{ self.clear() self.setFocus(Qt.FocusReason.OtherFocusReason) + def show_parse_error_action(self, to_show, tooltip=''): + try: + self.parse_error_action.setVisible(to_show) + self.parse_error_action.setToolTip(tooltip) + except Exception: + pass + def search_done(self, ok): if isinstance(ok, string_or_bytes): self.setToolTip(ok) + self.show_parse_error_action(True, tooltip=ok) ok = False if not str(self.currentText()).strip(): self.clear(emit_search=False) @@ -223,6 +233,7 @@ class SearchBox2(QComboBox): # {{{ # Comes from the combobox itself def keyPressEvent(self, event): + self.show_parse_error_action(False) k = event.key() if k in (Qt.Key.Key_Enter, Qt.Key.Key_Return): return self.do_search()