diff --git a/src/calibre/gui2/viewer/search.py b/src/calibre/gui2/viewer/search.py index 052d75cb70..e6df09cd0c 100644 --- a/src/calibre/gui2/viewer/search.py +++ b/src/calibre/gui2/viewer/search.py @@ -123,16 +123,18 @@ class SearchFinished(object): self.search_query = search_query -class SearchResult(object): +class SearchResult: __slots__ = ( 'search_query', 'before', 'text', 'after', 'q', 'spine_idx', - 'index', 'file_name', 'is_hidden', 'offset', 'toc_nodes' + 'index', 'file_name', 'is_hidden', 'offset', 'toc_nodes', + 'result_num' ) - def __init__(self, search_query, before, text, after, q, name, spine_idx, index, offset): + def __init__(self, search_query, before, text, after, q, name, spine_idx, index, offset, result_num): self.search_query = search_query self.q = q + self.result_num = result_num self.before, self.text, self.after = before, text, after self.spine_idx, self.index = spine_idx, index self.file_name = name @@ -149,7 +151,8 @@ class SearchResult(object): def for_js(self): return { 'file_name': self.file_name, 'spine_idx': self.spine_idx, 'index': self.index, 'text': self.text, - 'before': self.before, 'after': self.after, 'mode': self.search_query.mode, 'q': self.q + 'before': self.before, 'after': self.after, 'mode': self.search_query.mode, 'q': self.q, + 'result_num': self.result_num } def is_result(self, result_from_js): @@ -514,7 +517,6 @@ class Results(QTreeWidget): # {{{ self.search_results.append(result) n = self.number_of_results self.count_changed.emit(n) - return n def item_activated(self): i = self.currentItem() @@ -545,6 +547,14 @@ class Results(QTreeWidget): # {{{ item.setIcon(0, self.not_found_icon) break + def search_result_discovered(self, sr): + q = sr['result_num'] + for i in range(self.number_of_results): + item = self.item_map[i] + r = item.data(0, SEARCH_RESULT_ROLE) + if r.result_num == q: + self.setCurrentItem(item) + @property def current_result_is_hidden(self): item = self.currentItem() @@ -588,6 +598,7 @@ class SearchPanel(QWidget): # {{{ def __init__(self, parent=None): QWidget.__init__(self, parent) + self.discovery_counter = 0 self.last_hidden_text_warning = None self.current_search = None self.anchor_cfi = None @@ -643,6 +654,7 @@ class SearchPanel(QWidget): # {{{ self.current_search = search_query self.last_hidden_text_warning = None self.search_tasks.put((search_query, current_name)) + self.discovery_counter += 1 def set_anchor_cfi(self, pos_data): self.anchor_cfi = pos_data['cfi'] @@ -666,6 +678,7 @@ class SearchPanel(QWidget): # {{{ self.results_found.emit(SearchFinished(search_query)) continue num_in_spine = len(spine) + result_num = 0 for n in range(num_in_spine): idx = (spine_idx + n) % num_in_spine name = spine[idx] @@ -674,7 +687,8 @@ class SearchPanel(QWidget): # {{{ for i, result in enumerate(search_in_name(name, search_query)): before, text, after, offset = result q = (before or '')[-5:] + text + (after or '')[:5] - self.results_found.emit(SearchResult(search_query, before, text, after, q, name, idx, counter[q], offset)) + result_num += 1 + self.results_found.emit(SearchResult(search_query, before, text, after, q, name, idx, counter[q], offset, result_num)) counter[q] += 1 except Exception: import traceback @@ -691,10 +705,10 @@ class SearchPanel(QWidget): # {{{ else: self.show_no_results_found() return - if self.results.add_result(result) == 1: - # first result - self.results.select_first_result() - self.results.item_activated() + self.results.add_result(result) + obj = result.for_js + obj['on_discovery'] = self.discovery_counter + self.show_search_result.emit(obj) self.update_hidden_message() def visibility_changed(self, visible): @@ -730,6 +744,9 @@ class SearchPanel(QWidget): # {{{ self.results.search_result_not_found(sr) self.update_hidden_message() + def search_result_discovered(self, sr): + self.results.search_result_discovered(sr) + def show_no_results_found(self): msg = _('No matches were found for:') warning_dialog(self, _('No matches found'), msg + ' {}'.format(self.current_search.text), show=True) diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index d09974a49d..029b55ee97 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -168,6 +168,7 @@ class EbookViewer(MainWindow): self.web_view.find_next.connect(self.search_widget.find_next_requested) self.search_widget.show_search_result.connect(self.web_view.show_search_result) self.web_view.search_result_not_found.connect(self.search_widget.search_result_not_found) + self.web_view.search_result_discovered.connect(self.search_widget.search_result_discovered) self.web_view.toggle_bookmarks.connect(self.toggle_bookmarks) self.web_view.toggle_highlights.connect(self.toggle_highlights) self.web_view.new_bookmark.connect(self.bookmarks_widget.create_new_bookmark) diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 9a8d86ad2e..c5fb33a0e0 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -248,6 +248,7 @@ class ViewerBridge(Bridge): toggle_lookup = from_js(object) show_search = from_js(object, object) search_result_not_found = from_js(object) + search_result_discovered = from_js(object) find_next = from_js(object) quit = from_js() update_current_toc_nodes = from_js(object) @@ -449,6 +450,7 @@ class WebView(RestartingWebEngineView): toggle_toc = pyqtSignal() show_search = pyqtSignal(object, object) search_result_not_found = pyqtSignal(object) + search_result_discovered = pyqtSignal(object) find_next = pyqtSignal(object) toggle_bookmarks = pyqtSignal() toggle_highlights = pyqtSignal() @@ -507,6 +509,7 @@ class WebView(RestartingWebEngineView): self.bridge.toggle_toc.connect(self.toggle_toc) self.bridge.show_search.connect(self.show_search) self.bridge.search_result_not_found.connect(self.search_result_not_found) + self.bridge.search_result_discovered.connect(self.search_result_discovered) self.bridge.find_next.connect(self.find_next) self.bridge.toggle_bookmarks.connect(self.toggle_bookmarks) self.bridge.toggle_highlights.connect(self.toggle_highlights) diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 3a26259ace..5b933a152c 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -110,6 +110,17 @@ class EPUBReadingSystem: return self.__repr__() +class FullBookSearch: + + def __init__(self): + start_spine_index = current_spine_item()?.index + if not start_spine_index?: + start_spine_index = -1 + self.start_spine_index = start_spine_index + self.progress_frac_at_start = progress_frac() + self.first_result_shown = False + + class IframeBoss: def __init__(self): @@ -122,6 +133,7 @@ class IframeBoss: self.content_ready = False self.last_window_width = self.last_window_height = -1 self.forward_keypresses = False + self.full_book_search_in_progress = None set_boss(self) handlers = { 'change_color_scheme': self.change_color_scheme, @@ -746,9 +758,24 @@ class IframeBoss: self.send_message(msg_type, text=data.text, backwards=data.backwards, searched_in_spine=data.searched_in_spine) def show_search_result(self, data, from_load): + sr = data.search_result + if sr.on_discovery: + if sr.result_num is 1: + self.full_book_search_in_progress = FullBookSearch() + elif self.full_book_search_in_progress?.first_result_shown: + return self.last_search_at = window.performance.now() - if select_search_result(data.search_result): + x, y = scroll_viewport.x(), scroll_viewport.y() + if select_search_result(sr): self.ensure_selection_visible() + if self.full_book_search_in_progress and not self.full_book_search_in_progress.first_result_shown and sr.on_discovery: + discovered = False + if progress_frac() >= self.full_book_search_in_progress.progress_frac_at_start or current_spine_item().index is not self.full_book_search_in_progress.start_spine_index: + self.full_book_search_in_progress.first_result_shown = True + discovered = True + else: + scroll_viewport.scroll_to(x, y) + self.send_message('search_result_discovered', search_result=data.search_result, discovered=discovered) else: self.send_message('search_result_not_found', search_result=data.search_result) diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 9771c9f88c..863707fe41 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -287,10 +287,8 @@ class View: 'update_cfi': self.on_update_cfi, 'update_progress_frac': self.on_update_progress_frac, 'update_toc_position': self.on_update_toc_position, - 'search_result_not_found': def (data): - if ui_operations.search_result_not_found: - ui_operations.search_result_not_found(data.search_result) - , + 'search_result_not_found': self.search_result_not_found, + 'search_result_discovered': self.search_result_discovered, 'annotations': self.on_annotations_message, 'tts': self.on_tts_message, 'hints': self.on_hints_message, @@ -1374,11 +1372,39 @@ class View: if ui_operations.reference_mode_changed: ui_operations.reference_mode_changed(self.reference_mode_enabled) + def discover_search_result(self, sr): + if sr.result_num is 1: + self.search_result_discovery = {'queue': v'[]', 'on_discovery': sr.on_discovery, 'in_flight': None, 'discovered': False} + if not self.search_result_discovery or self.search_result_discovery.discovered or self.search_result_discovery.on_discovery is not sr.on_discovery: + return + self.search_result_discovery.queue.push(sr) + if not self.search_result_discovery.in_flight: + self.show_search_result(self.search_result_discovery.queue.shift()) + + def handle_search_result_discovery(self, sr, discovered): + if self.search_result_discovery?.on_discovery is sr.on_discovery: + self.search_result_discovery.in_flight = None + if discovered: + self.search_result_discovery.discovered = True + ui_operations.search_result_discovered(sr) + elif not self.search_result_discovery.discovered and self.search_result_discovery.queue.length: + self.show_search_result(self.search_result_discovery.queue.shift()) + + def search_result_discovered(self, data): + self.handle_search_result_discovery(data.search_result, data.discovered) + + def search_result_not_found(self, data): + if ui_operations.search_result_not_found: + ui_operations.search_result_not_found(data.search_result) + self.handle_search_result_discovery(data.search_result, False) + def show_search_result(self, sr): if self.currently_showing.name is sr.file_name: self.iframe_wrapper.send_message('show_search_result', search_result=sr) else: self.show_name(sr.file_name, initial_position={'type':'search_result', 'search_result':sr, 'replace_history':True}) + if self.search_result_discovery?.on_discovery is sr.on_discovery: + self.search_result_discovery.in_flight = sr.result_num def highlight_action(self, uuid, which): spine = self.book.manifest.spine diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index 56afbf580b..c35ff2a80c 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -270,7 +270,10 @@ def trigger_shortcut(which): @from_python def show_search_result(sr): if view: - view.show_search_result(sr) + if sr.on_discovery: + view.discover_search_result(sr) + else: + view.show_search_result(sr) @from_python def prepare_for_close(): @@ -403,6 +406,8 @@ if window is window.top: to_python.read_aloud_state_changed(active) ui_operations.search_result_not_found = def(sr): to_python.search_result_not_found(sr) + ui_operations.search_result_discovered = def(sr): + to_python.search_result_discovered(sr) ui_operations.scrollbar_context_menu = def(x, y, frac): to_python.scrollbar_context_menu(x, y, frac) ui_operations.close_prep_finished = def(cfi):