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 consistentsymbol_search
parent
c478ddaed0
commit
89beb92866
|
@ -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,24 +295,90 @@ 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:
|
||||||
|
# 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.clear()
|
||||||
|
|
||||||
model.setHorizontalHeaderLabels(self.labels)
|
model.setHorizontalHeaderLabels(self.labels)
|
||||||
|
|
||||||
|
|
||||||
|
section_idx = self.clear_section(section)
|
||||||
|
|
||||||
|
# for key, values in results.items():
|
||||||
|
|
||||||
|
if section_idx is None:
|
||||||
root = model.invisibleRootItem()
|
root = model.invisibleRootItem()
|
||||||
|
section_item = QStandardItem(section)
|
||||||
for key, values in results.items():
|
root.appendRow(section_item)
|
||||||
|
else:
|
||||||
src = QStandardItem(key)
|
section_item = model.itemFromIndex(section_idx)
|
||||||
root.appendRow(src)
|
|
||||||
|
|
||||||
# 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):
|
||||||
|
@ -324,36 +387,21 @@ class CompleterView(QTreeView):
|
||||||
item = QStandardItem(s)
|
item = QStandardItem(s)
|
||||||
|
|
||||||
# Add the item to the model
|
# Add the item to the model
|
||||||
src.appendRow([ix, item])
|
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,14 +639,35 @@ 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:
|
||||||
|
# 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()
|
bar.show()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue