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.symbol_search
parent
63363d750c
commit
b2ff09f193
|
@ -38,6 +38,7 @@ from typing import (
|
||||||
Awaitable, Sequence, Dict,
|
Awaitable, Sequence, Dict,
|
||||||
Any, AsyncIterator, Tuple,
|
Any, AsyncIterator, Tuple,
|
||||||
)
|
)
|
||||||
|
import time
|
||||||
# from pprint import pformat
|
# from pprint import pformat
|
||||||
|
|
||||||
from fuzzywuzzy import process as fuzzy
|
from fuzzywuzzy import process as fuzzy
|
||||||
|
@ -207,7 +208,6 @@ class CompleterView(QTreeView):
|
||||||
# values just needs to be sequence-like
|
# values just needs to be sequence-like
|
||||||
for i, s in enumerate(values):
|
for i, s in enumerate(values):
|
||||||
|
|
||||||
# blank = QStandardItem('')
|
|
||||||
ix = QStandardItem(str(i))
|
ix = QStandardItem(str(i))
|
||||||
item = QStandardItem(s)
|
item = QStandardItem(s)
|
||||||
# item.setCheckable(False)
|
# item.setCheckable(False)
|
||||||
|
@ -220,16 +220,20 @@ class CompleterView(QTreeView):
|
||||||
# XXX: these 2 lines MUST be in sequence in order
|
# XXX: these 2 lines MUST be in sequence in order
|
||||||
# to get the view to show right after typing input.
|
# to get the view to show right after typing input.
|
||||||
sel = self.selectionModel()
|
sel = self.selectionModel()
|
||||||
|
|
||||||
|
# select row without selecting.. :eye_rollzz:
|
||||||
|
# https://doc.qt.io/qt-5/qabstractitemview.html#setCurrentIndex
|
||||||
sel.setCurrentIndex(
|
sel.setCurrentIndex(
|
||||||
model.index(0, 0, QModelIndex()),
|
model.index(0, 0, QModelIndex()),
|
||||||
QItemSelectionModel.ClearAndSelect |
|
QItemSelectionModel.ClearAndSelect |
|
||||||
QItemSelectionModel.Rows
|
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()))
|
self.select_from_idx(model.index(0, 0, QModelIndex()))
|
||||||
|
|
||||||
|
|
||||||
def show_matches(self) -> None:
|
def show_matches(self) -> None:
|
||||||
# print(f"SHOWING {self}")
|
|
||||||
self.show()
|
self.show()
|
||||||
self.resize()
|
self.resize()
|
||||||
|
|
||||||
|
@ -242,7 +246,6 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
# inclusive of search bar and header "rows" in pixel terms
|
# inclusive of search bar and header "rows" in pixel terms
|
||||||
rows = 100
|
rows = 100
|
||||||
# print(f'row count: {rows}')
|
|
||||||
# max_rows = 8 # 6 + search and headers
|
# max_rows = 8 # 6 + search and headers
|
||||||
row_px = self.rowHeight(self.currentIndex())
|
row_px = self.rowHeight(self.currentIndex())
|
||||||
# print(f'font_h: {font_h}\n px_height: {px_height}')
|
# print(f'font_h: {font_h}\n px_height: {px_height}')
|
||||||
|
@ -374,7 +377,11 @@ async def fill_results(
|
||||||
symsearch: Callable[..., Awaitable],
|
symsearch: Callable[..., Awaitable],
|
||||||
recv_chan: trio.abc.ReceiveChannel,
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
# cached_symbols: Dict[str,
|
# 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:
|
) -> None:
|
||||||
"""Task to search through providers and fill in possible
|
"""Task to search through providers and fill in possible
|
||||||
|
@ -385,26 +392,28 @@ async def fill_results(
|
||||||
|
|
||||||
bar = search.bar
|
bar = search.bar
|
||||||
view = bar.view
|
view = bar.view
|
||||||
sel = bar.view.selectionModel()
|
|
||||||
model = bar.view.model()
|
|
||||||
|
|
||||||
last_search_text = ''
|
|
||||||
last_text = bar.text()
|
last_text = bar.text()
|
||||||
repeats = 0
|
repeats = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
await _search_active.wait()
|
||||||
|
period = None
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
last_text = bar.text()
|
last_text = bar.text()
|
||||||
await _search_active.wait()
|
wait_start = time.time()
|
||||||
|
|
||||||
with trio.move_on_after(pause_time) as cs:
|
with trio.move_on_after(max_pause_time):
|
||||||
# cs.shield = True
|
|
||||||
pattern = await recv_chan.receive()
|
pattern = await recv_chan.receive()
|
||||||
print(pattern)
|
|
||||||
|
period = time.time() - wait_start
|
||||||
|
print(f'{pattern} after {period}')
|
||||||
|
|
||||||
# during fast multiple key inputs, wait until a pause
|
# during fast multiple key inputs, wait until a pause
|
||||||
# (in typing) to initiate search
|
# (in typing) to initiate search
|
||||||
if not cs.cancelled_caught:
|
if period < min_pause_time:
|
||||||
log.debug(f'Ignoring fast input for {pattern}')
|
log.debug(f'Ignoring fast input for {pattern}')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -414,49 +423,32 @@ async def fill_results(
|
||||||
if not text:
|
if not text:
|
||||||
print('idling')
|
print('idling')
|
||||||
_search_active = trio.Event()
|
_search_active = trio.Event()
|
||||||
continue
|
break
|
||||||
|
|
||||||
|
if repeats > 2 and period >= max_pause_time:
|
||||||
|
_search_active = trio.Event()
|
||||||
|
repeats = 0
|
||||||
|
break
|
||||||
|
|
||||||
if text == last_text:
|
if text == last_text:
|
||||||
repeats += 1
|
repeats += 1
|
||||||
|
|
||||||
if repeats > 1:
|
|
||||||
_search_active = trio.Event()
|
|
||||||
repeats = 0
|
|
||||||
|
|
||||||
if not _search_enabled:
|
if not _search_enabled:
|
||||||
print('search not ENABLED?')
|
print('search currently disabled')
|
||||||
continue
|
break
|
||||||
|
|
||||||
if last_search_text and last_search_text == text:
|
|
||||||
continue
|
|
||||||
|
|
||||||
log.debug(f'Search req for {text}')
|
log.debug(f'Search req for {text}')
|
||||||
|
|
||||||
last_search_text = text
|
results = await symsearch(text, period=period)
|
||||||
results = await symsearch(text)
|
|
||||||
log.debug(f'Received search result {results}')
|
log.debug(f'Received search result {results}')
|
||||||
|
|
||||||
if results and _search_enabled:
|
if results and _search_enabled:
|
||||||
|
|
||||||
# TODO: indented branch results for each provider
|
# show the results in the completer view
|
||||||
view.set_results(results)
|
view.set_results(results)
|
||||||
|
|
||||||
# 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()))
|
|
||||||
|
|
||||||
# sel.setCurrentIndex(
|
|
||||||
# model.index(0, 0, QModelIndex()),
|
|
||||||
# QItemSelectionModel.ClearAndSelect |
|
|
||||||
# QItemSelectionModel.Rows
|
|
||||||
# )
|
|
||||||
|
|
||||||
bar.show()
|
bar.show()
|
||||||
|
|
||||||
# # ensure we select first indented entry
|
|
||||||
# view.select_from_idx(sel.currentIndex())
|
|
||||||
|
|
||||||
|
|
||||||
class SearchWidget(QtGui.QWidget):
|
class SearchWidget(QtGui.QWidget):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -491,21 +483,18 @@ class SearchWidget(QtGui.QWidget):
|
||||||
view=self.view,
|
view=self.view,
|
||||||
)
|
)
|
||||||
self.vbox.addWidget(self.bar)
|
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.addWidget(self.bar.view)
|
||||||
self.vbox.setAlignment(self.view, Qt.AlignTop | Qt.AlignLeft)
|
self.vbox.setAlignment(self.view, Qt.AlignTop | Qt.AlignLeft)
|
||||||
|
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
# fill cache list
|
# fill cache list
|
||||||
self.view.set_results({'cache': list(self.chart_app._chart_cache)})
|
self.view.set_results({'cache': list(self.chart_app._chart_cache)})
|
||||||
self.bar.focus()
|
self.bar.focus()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_keyboard_input(
|
async def handle_keyboard_input(
|
||||||
|
|
||||||
# chart: 'ChartSpace', # type: igore # noqa
|
|
||||||
search: SearchWidget,
|
search: SearchWidget,
|
||||||
recv_chan: trio.abc.ReceiveChannel,
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
keyboard_pause_period: float = 0.0616,
|
keyboard_pause_period: float = 0.0616,
|
||||||
|
@ -533,7 +522,6 @@ async def handle_keyboard_input(
|
||||||
search,
|
search,
|
||||||
symsearch,
|
symsearch,
|
||||||
recv,
|
recv,
|
||||||
pause_time=keyboard_pause_period,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -601,21 +589,24 @@ async def handle_keyboard_input(
|
||||||
elif key == Qt.Key_J:
|
elif key == Qt.Key_J:
|
||||||
nidx = view.select_next()
|
nidx = view.select_next()
|
||||||
|
|
||||||
# select row without selecting.. :eye_rollzz:
|
|
||||||
# https://doc.qt.io/qt-5/qabstractitemview.html#setCurrentIndex
|
|
||||||
if nidx.isValid():
|
if nidx.isValid():
|
||||||
i, item = view.select_from_idx(nidx)
|
i, item = view.select_from_idx(nidx)
|
||||||
|
|
||||||
if item:
|
if item:
|
||||||
parent_item = item.parent()
|
parent_item = item.parent()
|
||||||
if parent_item and parent_item.text() == 'cache':
|
if parent_item and parent_item.text() == 'cache':
|
||||||
node = model.itemFromIndex(i.siblingAtColumn(1))
|
node = model.itemFromIndex(
|
||||||
|
i.siblingAtColumn(1)
|
||||||
|
)
|
||||||
if node:
|
if node:
|
||||||
|
|
||||||
|
# TODO: parse out provider from
|
||||||
|
# cached value.
|
||||||
value = node.text()
|
value = node.text()
|
||||||
print(f'cache selection')
|
|
||||||
search.chart_app.load_symbol(
|
search.chart_app.load_symbol(
|
||||||
app.linkedcharts.symbol.brokers[0],
|
app.linkedcharts.symbol.brokers[0],
|
||||||
value,
|
node.text(),
|
||||||
'info',
|
'info',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -631,10 +622,6 @@ async def search_simple_dict(
|
||||||
source: dict,
|
source: dict,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
|
||||||
# matches_per_src = {}
|
|
||||||
|
|
||||||
# for source, data in source.items():
|
|
||||||
|
|
||||||
# search routine can be specified as a function such
|
# search routine can be specified as a function such
|
||||||
# as in the case of the current app's local symbol cache
|
# as in the case of the current app's local symbol cache
|
||||||
matches = fuzzy.extractBests(
|
matches = fuzzy.extractBests(
|
||||||
|
@ -656,6 +643,8 @@ def get_multi_search() -> Callable[..., Awaitable]:
|
||||||
|
|
||||||
async def multisearcher(
|
async def multisearcher(
|
||||||
pattern: str,
|
pattern: str,
|
||||||
|
period: str,
|
||||||
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
||||||
matches = {}
|
matches = {}
|
||||||
|
@ -664,7 +653,9 @@ def get_multi_search() -> Callable[..., Awaitable]:
|
||||||
provider: str,
|
provider: str,
|
||||||
pattern: str,
|
pattern: str,
|
||||||
search: Callable[..., Awaitable[dict]],
|
search: Callable[..., Awaitable[dict]],
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
log.debug(f'Searching {provider} for "{pattern}"')
|
log.debug(f'Searching {provider} for "{pattern}"')
|
||||||
results = await search(pattern)
|
results = await search(pattern)
|
||||||
if results:
|
if results:
|
||||||
|
@ -673,8 +664,14 @@ def get_multi_search() -> Callable[..., Awaitable]:
|
||||||
# TODO: make this an async stream?
|
# TODO: make this an async stream?
|
||||||
async with trio.open_nursery() as n:
|
async with trio.open_nursery() as n:
|
||||||
|
|
||||||
for brokername, search in _searcher_cache.items():
|
for provider, (search, min_pause) in _searcher_cache.items():
|
||||||
n.start_soon(pack_matches, brokername, pattern, search)
|
|
||||||
|
# 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
|
return matches
|
||||||
|
|
||||||
|
@ -686,15 +683,19 @@ async def register_symbol_search(
|
||||||
|
|
||||||
provider_name: str,
|
provider_name: str,
|
||||||
search_routine: Callable,
|
search_routine: Callable,
|
||||||
|
pause_period: Optional[float] = None,
|
||||||
|
|
||||||
) -> AsyncIterator[dict]:
|
) -> AsyncIterator[dict]:
|
||||||
|
|
||||||
global _searcher_cache
|
global _searcher_cache
|
||||||
|
|
||||||
|
pause_period = pause_period or 0.061
|
||||||
|
|
||||||
# deliver search func to consumer
|
# deliver search func to consumer
|
||||||
try:
|
try:
|
||||||
_searcher_cache[provider_name] = search_routine
|
_searcher_cache[provider_name] = (search_routine, pause_period)
|
||||||
yield search_routine
|
yield search_routine
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
_searcher_cache.pop(provider_name)
|
_searcher_cache.pop(provider_name)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue