Add api for per-section filling/clearing

Makes it so we can move toward separate provider results fills in an
async way, on demand.

Also,
- add depth 1 iteration helper method
- add section finder helper method
- fix last selection loading to be mostly consistent
symbol_search
Tyler Goodlet 2021-05-26 13:49:14 -04:00
parent c478ddaed0
commit 89beb92866
1 changed files with 163 additions and 92 deletions

View File

@ -31,6 +31,7 @@ qompleterz: embeddable search and complete using trio, Qt and fuzzywuzzy.
# https://github.com/qutebrowser/qutebrowser/blob/master/qutebrowser/completion/completiondelegate.py#L243 # https://github.com/qutebrowser/qutebrowser/blob/master/qutebrowser/completion/completiondelegate.py#L243
# https://forum.qt.io/topic/61343/highlight-matched-substrings-in-qstyleditemdelegate # https://forum.qt.io/topic/61343/highlight-matched-substrings-in-qstyleditemdelegate
from collections import defaultdict
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from functools import partial from functools import partial
from typing import ( from typing import (
@ -91,18 +92,6 @@ class SimpleDelegate(QStyledItemDelegate):
super().__init__(parent) super().__init__(parent)
self.dpi_font = font self.dpi_font = font
# def sizeHint(self, *args) -> QtCore.QSize:
# """
# Scale edit box to size of dpi aware font.
# """
# psh = super().sizeHint(*args)
# # psh.setHeight(self.dpi_font.px_size + 2)
# psh.setHeight(18)
# # psh.setHeight(18)
# return psh
class CompleterView(QTreeView): class CompleterView(QTreeView):
@ -141,6 +130,12 @@ class CompleterView(QTreeView):
self.setAnimated(False) self.setAnimated(False)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# TODO: this up front?
# self.setSelectionModel(
# QItemSelectionModel.ClearAndSelect |
# QItemSelectionModel.Rows
# )
# self.setVerticalBarPolicy(Qt.ScrollBarAlwaysOff) # self.setVerticalBarPolicy(Qt.ScrollBarAlwaysOff)
# self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored) # self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
@ -148,7 +143,6 @@ class CompleterView(QTreeView):
model.setHorizontalHeaderLabels(labels) model.setHorizontalHeaderLabels(labels)
self._font_size: int = 0 # pixels self._font_size: int = 0 # pixels
# self._cache: Dict[str, List[str]] = {}
# def viewportSizeHint(self) -> QtCore.QSize: # def viewportSizeHint(self) -> QtCore.QSize:
# vps = super().viewportSizeHint() # vps = super().viewportSizeHint()
@ -178,10 +172,6 @@ class CompleterView(QTreeView):
self.setStyleSheet(f"font: {size}px") self.setStyleSheet(f"font: {size}px")
def show_matches(self) -> None:
self.show()
self.resize()
def resize(self): def resize(self):
model = self.model() model = self.model()
cols = model.columnCount() cols = model.columnCount()
@ -200,6 +190,10 @@ class CompleterView(QTreeView):
self.setMaximumSize(self.width() + 10, rows * row_px) self.setMaximumSize(self.width() + 10, rows * row_px)
self.setFixedWidth(333) self.setFixedWidth(333)
def is_selecting_d1(self) -> bool:
cidx = self.selectionModel().currentIndex()
return cidx.parent() == QModelIndex()
def previous_index(self) -> QModelIndex: def previous_index(self) -> QModelIndex:
cidx = self.selectionModel().currentIndex() cidx = self.selectionModel().currentIndex()
@ -264,9 +258,12 @@ class CompleterView(QTreeView):
''' '''
# ensure we're **not** selecting the first level parent node and # ensure we're **not** selecting the first level parent node and
# instead its child. # instead its child.
return self.select_from_idx( model = self.model()
self.indexBelow(self.model().index(0, 0, QModelIndex())) for idx, item in self.iter_d1():
) if model.rowCount(idx) == 0:
continue
else:
return self.select_from_idx(self.indexBelow(idx))
def select_next(self) -> QStandardItem: def select_next(self) -> QStandardItem:
idx = self.next_index() idx = self.next_index()
@ -298,62 +295,113 @@ class CompleterView(QTreeView):
self.select_from_idx(nidx) self.select_from_idx(nidx)
return self.select_next() return self.select_next()
def set_results( def iter_d1(
self, self,
results: Dict[str, Sequence[str]], ) -> tuple[QModelIndex, QStandardItem]:
) -> None:
model = self.model() model = self.model()
isections = model.rowCount()
# XXX: currently we simply rewrite the model from scratch each call # much thanks to following code to figure out breadth-first
# since it seems to be super fast anyway. # traversing from the root node:
model.clear() # https://stackoverflow.com/a/33126689
for i in range(isections):
idx = model.index(i, 0, QModelIndex())
item = model.itemFromIndex(idx)
yield idx, item
def find_section(
self,
section: str,
) -> Optional[QModelIndex]:
'''Find the *first* depth = 1 section matching ``section`` in
the tree and return its index.
'''
for idx, item in self.iter_d1():
if item.text() == section:
return idx
else:
# caller must expect his
return None
def clear_section(
self,
section: str,
status_field: str = None,
) -> None:
'''Clear all result-rows from under the depth = 1 section.
'''
idx = self.find_section(section)
model = self.model()
if idx is not None:
if model.hasChildren(idx):
rows = model.rowCount(idx)
# print(f'removing {rows} from {section}')
assert model.removeRows(0, rows, parent=idx)
# remove section as well
# model.removeRow(i, QModelIndex())
return idx
else:
return None
def set_section_entries(
self,
section: str,
values: Sequence[str],
clear_all: bool = False,
) -> None:
'''Set result-rows for depth = 1 tree section ``section``.
'''
model = self.model()
if clear_all:
# XXX: rewrite the model from scratch if caller requests it
model.clear()
model.setHorizontalHeaderLabels(self.labels) model.setHorizontalHeaderLabels(self.labels)
root = model.invisibleRootItem()
for key, values in results.items():
src = QStandardItem(key) section_idx = self.clear_section(section)
root.appendRow(src)
# values just needs to be sequence-like # for key, values in results.items():
for i, s in enumerate(values):
ix = QStandardItem(str(i)) if section_idx is None:
item = QStandardItem(s) root = model.invisibleRootItem()
section_item = QStandardItem(section)
root.appendRow(section_item)
else:
section_item = model.itemFromIndex(section_idx)
# Add the item to the model # values just needs to be sequence-like
src.appendRow([ix, item]) for i, s in enumerate(values):
ix = QStandardItem(str(i))
item = QStandardItem(s)
# Add the item to the model
section_item.appendRow([ix, item])
self.expandAll() self.expandAll()
# XXX: these 2 lines MUST be in sequence in order # XXX: THE BELOW LINE MUST BE CALLED.
# to get the view to show right after typing input. # this stuff is super finicky and if not done right will cause
sel = self.selectionModel() # Qt crashes out our buttz. it's required in order to get the
# view to show right after typing input.
# 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
)
self.select_first() self.select_first()
self.show_matches() self.show_matches()
# def find_matches( def show_matches(self) -> None:
# self, self.show()
# field: str, self.resize()
# txt: str,
# ) -> List[QStandardItem]:
# model = self.model()
# items = model.findItems(
# txt,
# Qt.MatchContains,
# self.field_to_col(field),
# )
class SearchBar(QtWidgets.QLineEdit): class SearchBar(QtWidgets.QLineEdit):
@ -480,8 +528,11 @@ class SearchWidget(QtGui.QWidget):
if self.view.model().rowCount(QModelIndex()) == 0: if self.view.model().rowCount(QModelIndex()) == 0:
# fill cache list if nothing existing # fill cache list if nothing existing
self.view.set_results( self.view.set_section_entries(
{'cache': list(reversed(self.chart_app._chart_cache))}) 'cache',
list(reversed(self.chart_app._chart_cache)),
clear_all=True,
)
self.bar.focus() self.bar.focus()
self.show() self.show()
@ -521,7 +572,7 @@ _search_enabled: bool = False
async def fill_results( async def fill_results(
search: SearchBar, search: SearchBar,
symsearch: Callable[..., Awaitable], # multisearch: Callable[..., Awaitable],
recv_chan: trio.abc.ReceiveChannel, recv_chan: trio.abc.ReceiveChannel,
# kb debouncing pauses # kb debouncing pauses
@ -535,11 +586,15 @@ async def fill_results(
""" """
global _search_active, _search_enabled global _search_active, _search_enabled
multisearch = get_multi_search()
bar = search.bar bar = search.bar
view = bar.view view = bar.view
view.select_from_idx(QModelIndex())
last_text = bar.text() last_text = bar.text()
repeats = 0 repeats = 0
last_patt = None
while True: while True:
await _search_active.wait() await _search_active.wait()
@ -584,15 +639,36 @@ async def fill_results(
log.debug(f'Search req for {text}') log.debug(f'Search req for {text}')
results = await symsearch(text, period=period) # issue multi-provider fan-out search request
results = await multisearch(text, period=period)
log.debug(f'Received search result {results}') # matches = {}
# unmatches = []
if results and _search_enabled: if _search_enabled:
# show the results in the completer view for (provider, pattern), output in results.items():
view.set_results(results) if output:
bar.show() # matches[provider] = output
view.set_section_entries(
section=provider,
values=output,
)
else:
view.clear_section(provider)
if last_patt is None or last_patt != text:
view.select_first()
# only change select on first search iteration,
# late results from other providers should **not**
# move the current selection
# if pattern not in patt_searched:
# patt_searched[pattern].append(provider)
last_patt = text
bar.show()
async def handle_keyboard_input( async def handle_keyboard_input(
@ -610,17 +686,19 @@ async def handle_keyboard_input(
bar = search.bar bar = search.bar
view = bar.view view = bar.view
view.set_font_size(bar.dpi_font.px_size) view.set_font_size(bar.dpi_font.px_size)
# nidx = view.currentIndex()
symsearch = get_multi_search()
send, recv = trio.open_memory_channel(16) send, recv = trio.open_memory_channel(16)
async with trio.open_nursery() as n: async with trio.open_nursery() as n:
# start a background multi-searcher task which receives
# patterns relayed from this keyboard input handler and
# async updates the completer view's results.
n.start_soon( n.start_soon(
partial( partial(
fill_results, fill_results,
search, search,
symsearch, # multisearch,
recv, recv,
) )
) )
@ -633,10 +711,6 @@ async def handle_keyboard_input(
if mods == Qt.ControlModifier: if mods == Qt.ControlModifier:
ctl = True ctl = True
# alt = False
# if mods == Qt.AltModifier:
# alt = True
# # ctl + alt as combo # # ctl + alt as combo
# ctlalt = False # ctlalt = False
# if (QtCore.Qt.AltModifier | QtCore.Qt.ControlModifier) == mods: # if (QtCore.Qt.AltModifier | QtCore.Qt.ControlModifier) == mods:
@ -668,27 +742,24 @@ async def handle_keyboard_input(
chart.set_chart_symbol(fqsn, chart.linkedcharts) chart.set_chart_symbol(fqsn, chart.linkedcharts)
search.bar.clear() search.bar.clear()
view.set_results({ view.set_section_entries(
'cache': list(reversed(chart._chart_cache)) 'cache',
}) values=list(reversed(chart._chart_cache)),
clear_all=True,
)
_search_enabled = False _search_enabled = False
# release kb control of search bar
# search.bar.unfocus()
continue continue
elif not ctl and not bar.text(): elif not ctl and not bar.text():
# if nothing in search text show the cache # if nothing in search text show the cache
view.set_results({ view.set_section_entries(
'cache': list(reversed(chart._chart_cache)) 'cache',
}) list(reversed(chart._chart_cache)),
clear_all=True,
)
continue continue
# selection tips:
# - get parent node: search.index(row, 0)
# - first node index: index = search.index(0, 0, parent)
# - root node index: index = search.index(0, 0, QModelIndex())
# cancel and close # cancel and close
if ctl and key in { if ctl and key in {
Qt.Key_C, Qt.Key_C,
@ -789,7 +860,7 @@ def get_multi_search() -> Callable[..., Awaitable]:
period: str, period: str,
) -> dict: ) -> dict:
# nonlocal matches
matches = {} matches = {}
async def pack_matches( async def pack_matches(
@ -801,8 +872,8 @@ def get_multi_search() -> Callable[..., Awaitable]:
log.info(f'Searching {provider} for "{pattern}"') log.info(f'Searching {provider} for "{pattern}"')
results = await search(pattern) results = await search(pattern)
if results: # print(f'results from {provider}: {results}')
matches[provider] = results matches[(provider, pattern)] = results
# 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:
@ -811,7 +882,7 @@ def get_multi_search() -> Callable[..., Awaitable]:
# only conduct search on this backend if it's registered # only conduct search on this backend if it's registered
# for the corresponding pause period. # for the corresponding pause period.
if period >= min_pause: if period >= min_pause and (provider, pattern) not in matches:
# print( # print(
# f'searching {provider} after {period} > {min_pause}') # f'searching {provider} after {period} > {min_pause}')
n.start_soon(pack_matches, provider, pattern, search) n.start_soon(pack_matches, provider, pattern, search)