From b2ff09f1937427e35b892d767cc5def8e92c10f0 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 18 May 2021 08:19:52 -0400 Subject: [PATCH] Support min and max keyboard pauses Some providers do well with a "longer" debounce period (like ib) since searching them too frequently causes latency and stalls. By supporting both a min and max debounce period on keyboard input we can only send patterns to the slower engines when that period is triggered via `trio.move_on_after()` and continue to relay to faster engines when the measured period permits. Allow search routines to register their "min period" such that they can choose to ignore patterns that arrive before their heuristically known ideal wait. --- piker/ui/_search.py | 145 ++++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 72 deletions(-) diff --git a/piker/ui/_search.py b/piker/ui/_search.py index a3922d6b..79c545e0 100644 --- a/piker/ui/_search.py +++ b/piker/ui/_search.py @@ -38,6 +38,7 @@ from typing import ( Awaitable, Sequence, Dict, Any, AsyncIterator, Tuple, ) +import time # from pprint import pformat from fuzzywuzzy import process as fuzzy @@ -207,7 +208,6 @@ class CompleterView(QTreeView): # values just needs to be sequence-like for i, s in enumerate(values): - # blank = QStandardItem('') ix = QStandardItem(str(i)) item = QStandardItem(s) # item.setCheckable(False) @@ -220,16 +220,20 @@ class CompleterView(QTreeView): # XXX: these 2 lines MUST be in sequence in order # to get the view to show right after typing input. sel = self.selectionModel() + + # select row without selecting.. :eye_rollzz: + # https://doc.qt.io/qt-5/qabstractitemview.html#setCurrentIndex sel.setCurrentIndex( model.index(0, 0, QModelIndex()), QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows ) + + # ensure we're **not** selecting the first level parent node and + # instead its child. self.select_from_idx(model.index(0, 0, QModelIndex())) - def show_matches(self) -> None: - # print(f"SHOWING {self}") self.show() self.resize() @@ -242,7 +246,6 @@ class CompleterView(QTreeView): # inclusive of search bar and header "rows" in pixel terms rows = 100 - # print(f'row count: {rows}') # max_rows = 8 # 6 + search and headers row_px = self.rowHeight(self.currentIndex()) # print(f'font_h: {font_h}\n px_height: {px_height}') @@ -374,7 +377,11 @@ async def fill_results( symsearch: Callable[..., Awaitable], recv_chan: trio.abc.ReceiveChannel, # cached_symbols: Dict[str, - pause_time: float = 0.0616, + + # kb debouncing pauses + min_pause_time: float = 0.0616, + # long_pause_time: float = 0.4, + max_pause_time: float = 6/16, ) -> None: """Task to search through providers and fill in possible @@ -385,77 +392,62 @@ async def fill_results( bar = search.bar view = bar.view - sel = bar.view.selectionModel() - model = bar.view.model() - last_search_text = '' last_text = bar.text() repeats = 0 while True: - - last_text = bar.text() await _search_active.wait() + period = None - with trio.move_on_after(pause_time) as cs: - # cs.shield = True - pattern = await recv_chan.receive() - print(pattern) + while True: - # during fast multiple key inputs, wait until a pause - # (in typing) to initiate search - if not cs.cancelled_caught: - log.debug(f'Ignoring fast input for {pattern}') - continue + last_text = bar.text() + wait_start = time.time() - text = bar.text() - print(f'search: {text}') + with trio.move_on_after(max_pause_time): + pattern = await recv_chan.receive() - if not text: - print('idling') - _search_active = trio.Event() - continue + period = time.time() - wait_start + print(f'{pattern} after {period}') - if text == last_text: - repeats += 1 + # during fast multiple key inputs, wait until a pause + # (in typing) to initiate search + if period < min_pause_time: + log.debug(f'Ignoring fast input for {pattern}') + continue - if repeats > 1: - _search_active = trio.Event() - repeats = 0 + text = bar.text() + print(f'search: {text}') - if not _search_enabled: - print('search not ENABLED?') - continue + if not text: + print('idling') + _search_active = trio.Event() + break - if last_search_text and last_search_text == text: - continue + if repeats > 2 and period >= max_pause_time: + _search_active = trio.Event() + repeats = 0 + break - log.debug(f'Search req for {text}') + if text == last_text: + repeats += 1 - last_search_text = text - results = await symsearch(text) - log.debug(f'Received search result {results}') + if not _search_enabled: + print('search currently disabled') + break - if results and _search_enabled: + log.debug(f'Search req for {text}') - # TODO: indented branch results for each provider - view.set_results(results) + results = await symsearch(text, period=period) - # XXX: these 2 lines MUST be in sequence in order - # to get the view to show right after typing input. - # ensure we select first indented entry - # view.select_from_idx(model.index(0, 0, QModelIndex())) + log.debug(f'Received search result {results}') - # sel.setCurrentIndex( - # model.index(0, 0, QModelIndex()), - # QItemSelectionModel.ClearAndSelect | - # QItemSelectionModel.Rows - # ) + if results and _search_enabled: - bar.show() - - # # ensure we select first indented entry - # view.select_from_idx(sel.currentIndex()) + # show the results in the completer view + view.set_results(results) + bar.show() class SearchWidget(QtGui.QWidget): @@ -491,21 +483,18 @@ class SearchWidget(QtGui.QWidget): view=self.view, ) self.vbox.addWidget(self.bar) - self.vbox.setAlignment(self.bar, Qt.AlignTop | Qt.AlignLeft) + self.vbox.setAlignment(self.bar, Qt.AlignTop | Qt.AlignRight) self.vbox.addWidget(self.bar.view) self.vbox.setAlignment(self.view, Qt.AlignTop | Qt.AlignLeft) - def focus(self) -> None: # fill cache list self.view.set_results({'cache': list(self.chart_app._chart_cache)}) self.bar.focus() - async def handle_keyboard_input( - # chart: 'ChartSpace', # type: igore # noqa search: SearchWidget, recv_chan: trio.abc.ReceiveChannel, keyboard_pause_period: float = 0.0616, @@ -533,7 +522,6 @@ async def handle_keyboard_input( search, symsearch, recv, - pause_time=keyboard_pause_period, ) ) @@ -601,21 +589,24 @@ async def handle_keyboard_input( elif key == Qt.Key_J: nidx = view.select_next() - # select row without selecting.. :eye_rollzz: - # https://doc.qt.io/qt-5/qabstractitemview.html#setCurrentIndex if nidx.isValid(): i, item = view.select_from_idx(nidx) if item: parent_item = item.parent() if parent_item and parent_item.text() == 'cache': - node = model.itemFromIndex(i.siblingAtColumn(1)) + node = model.itemFromIndex( + i.siblingAtColumn(1) + ) if node: + + # TODO: parse out provider from + # cached value. value = node.text() - print(f'cache selection') + search.chart_app.load_symbol( app.linkedcharts.symbol.brokers[0], - value, + node.text(), 'info', ) @@ -631,10 +622,6 @@ async def search_simple_dict( source: dict, ) -> Dict[str, Any]: - # matches_per_src = {} - - # for source, data in source.items(): - # search routine can be specified as a function such # as in the case of the current app's local symbol cache matches = fuzzy.extractBests( @@ -656,6 +643,8 @@ def get_multi_search() -> Callable[..., Awaitable]: async def multisearcher( pattern: str, + period: str, + ) -> dict: matches = {} @@ -664,7 +653,9 @@ def get_multi_search() -> Callable[..., Awaitable]: provider: str, pattern: str, search: Callable[..., Awaitable[dict]], + ) -> None: + log.debug(f'Searching {provider} for "{pattern}"') results = await search(pattern) if results: @@ -673,8 +664,14 @@ def get_multi_search() -> Callable[..., Awaitable]: # TODO: make this an async stream? async with trio.open_nursery() as n: - for brokername, search in _searcher_cache.items(): - n.start_soon(pack_matches, brokername, pattern, search) + for provider, (search, min_pause) in _searcher_cache.items(): + + # only conduct search on this backend if it's registered + # for the corresponding pause period. + if period >= min_pause: + # print( + # f'searching {provider} after {period} > {min_pause}') + n.start_soon(pack_matches, provider, pattern, search) return matches @@ -686,15 +683,19 @@ async def register_symbol_search( provider_name: str, search_routine: Callable, + pause_period: Optional[float] = None, ) -> AsyncIterator[dict]: global _searcher_cache + pause_period = pause_period or 0.061 + # deliver search func to consumer try: - _searcher_cache[provider_name] = search_routine + _searcher_cache[provider_name] = (search_routine, pause_period) yield search_routine + finally: _searcher_cache.pop(provider_name)