Add initial working symbol search with async completions

symbol_search
Tyler Goodlet 2021-05-06 16:41:15 -04:00
parent ad494db213
commit 431fdd3f9c
1 changed files with 217 additions and 153 deletions

View File

@ -18,20 +18,19 @@
qompleterz: embeddable search and complete using trio, Qt and fuzzywuzzy. qompleterz: embeddable search and complete using trio, Qt and fuzzywuzzy.
""" """
from typing import List, Optional from typing import List, Optional, Callable, Awaitable
import sys import sys
# from pprint import pformat
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt, QEvent
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
import trio import trio
from PyQt5.QtCore import ( from PyQt5.QtCore import (
Qt, Qt,
QSize, # QSize,
QModelIndex, QModelIndex,
QItemSelectionModel, QItemSelectionModel,
# QStringListModel
) )
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QStandardItem, QStandardItem,
@ -43,15 +42,15 @@ from PyQt5.QtWidgets import (
QAbstractScrollArea, QAbstractScrollArea,
QStyledItemDelegate, QStyledItemDelegate,
) )
from fuzzywuzzy import process
from ..log import get_logger from ..log import get_logger
from ._style import ( from ._style import (
_font, _font,
DpiAwareFont, DpiAwareFont,
hcolor, # hcolor,
) )
from ..data import feed
log = get_logger(__name__) log = get_logger(__name__)
@ -90,10 +89,34 @@ class CompleterView(QTreeView):
def __init__( def __init__(
self, self,
parent=None, parent=None,
labels: List[str] = [],
) -> None: ) -> None:
super().__init__(parent) super().__init__(parent)
model = QStandardItemModel(self)
self.labels = labels
# a std "tabular" config
self.setItemDelegate(SimpleDelegate())
self.setModel(model)
self.setAlternatingRowColors(True)
self.setIndentation(1)
# self.setUniformRowHeights(True)
# self.setColumnWidth(0, 3)
# ux settings
self.setItemsExpandable(True)
self.setExpandsOnDoubleClick(False)
self.setAnimated(False)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
# column headers
model.setHorizontalHeaderLabels(labels)
self._font_size: int = 0 # pixels self._font_size: int = 0 # pixels
# self._cache: Dict[str, List[str]] = {} # self._cache: Dict[str, List[str]] = {}
@ -115,23 +138,6 @@ class CompleterView(QTreeView):
# # psh.setHeight(12) # # psh.setHeight(12)
# return QSize(-1, height) # return QSize(-1, height)
def resize(self):
model = self.model()
cols = model.columnCount()
for i in range(cols):
self.resizeColumnToContents(i)
# inclusive of search bar and header "rows" in pixel terms
rows = model.rowCount() + 2
# max_rows = 8 # 6 + search and headers
row_px = self.rowHeight(self.currentIndex())
# print(f'font_h: {font_h}\n px_height: {px_height}')
# TODO: probably make this more general / less hacky
self.setMinimumSize(self.width(), rows * row_px)
self.setMaximumSize(self.width(), rows * row_px)
def set_font_size(self, size: int = 18): def set_font_size(self, size: int = 18):
# dpi_px_size = _font.px_size # dpi_px_size = _font.px_size
print(size) print(size)
@ -148,6 +154,19 @@ class CompleterView(QTreeView):
) -> None: ) -> None:
model = self.model() model = self.model()
model.clear()
model.setHorizontalHeaderLabels(self.labels)
# TODO: wtf.. this model shit
# row_count = model.rowCount()
# if row_count > 0:
# model.removeRows(
# 0,
# row_count,
# # root index
# model.index(0, 0, QModelIndex()),
# )
for i, s in enumerate(results): for i, s in enumerate(results):
@ -160,49 +179,40 @@ class CompleterView(QTreeView):
# Add the item to the model # Add the item to the model
model.appendRow([src, ix, item]) model.appendRow([src, ix, item])
def find_matches( def show_matches(self) -> None:
self, # print(f"SHOWING {self}")
field: str, self.show()
txt: str, self.resize()
) -> List[QStandardItem]:
def resize(self):
model = self.model() model = self.model()
items = model.findItems( cols = model.columnCount()
txt,
Qt.MatchContains,
self.field_to_col(field),
)
for i in range(cols):
self.resizeColumnToContents(i)
def mk_completer_view( # inclusive of search bar and header "rows" in pixel terms
rows = model.rowCount() + 2
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}')
labels: List[str], # TODO: probably make this more general / less hacky
self.setMinimumSize(self.width(), rows * row_px)
self.setMaximumSize(self.width(), rows * row_px)
) -> QTreeView: # def find_matches(
# self,
tree = CompleterView() # field: str,
model = QStandardItemModel(tree) # txt: str,
# ) -> List[QStandardItem]:
# a std "tabular" config # model = self.model()
tree.setItemDelegate(SimpleDelegate()) # items = model.findItems(
tree.setModel(model) # txt,
tree.setAlternatingRowColors(True) # Qt.MatchContains,
tree.setIndentation(1) # self.field_to_col(field),
# )
# tree.setUniformRowHeights(True)
# tree.setColumnWidth(0, 3)
# ux settings
tree.setItemsExpandable(True)
tree.setExpandsOnDoubleClick(False)
tree.setAnimated(False)
tree.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
tree.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
# column headers
model.setHorizontalHeaderLabels(labels)
return tree
class FontSizedQLineEdit(QtWidgets.QLineEdit): class FontSizedQLineEdit(QtWidgets.QLineEdit):
@ -246,7 +256,7 @@ class FontSizedQLineEdit(QtWidgets.QLineEdit):
def show(self) -> None: def show(self) -> None:
super().show() super().show()
self.show_matches() self.view.show_matches()
# self.view.show() # self.view.show()
# self.view.resize() # self.view.resize()
@ -254,7 +264,7 @@ class FontSizedQLineEdit(QtWidgets.QLineEdit):
def view(self) -> CompleterView: def view(self) -> CompleterView:
if self._view is None: if self._view is None:
view = mk_completer_view(['src', 'i', 'symbol']) view = CompleterView(labels=['src', 'i', 'symbol'])
# print('yo') # print('yo')
# self.chart_app.vbox.addWidget(view) # self.chart_app.vbox.addWidget(view)
@ -264,13 +274,6 @@ class FontSizedQLineEdit(QtWidgets.QLineEdit):
return self._view return self._view
def show_matches(self):
view = self.view
view.set_font_size(self.dpi_font.px_size)
view.show()
# scale columns
view.resize()
def sizeHint(self) -> QtCore.QSize: def sizeHint(self) -> QtCore.QSize:
""" """
Scale edit box to size of dpi aware font. Scale edit box to size of dpi aware font.
@ -307,75 +310,131 @@ class FontSizedQLineEdit(QtWidgets.QLineEdit):
# # ev.accept() # # ev.accept()
async def fill_results(
search: FontSizedQLineEdit,
symsearch: Callable[..., Awaitable],
recv_chan: trio.abc.ReceiveChannel,
# pattern: str,
) -> None:
sel = search.view.selectionModel()
model = search.view.model()
async for pattern in recv_chan:
# so symbol search
# pattern = search.text()
# print(f'searching for: {pattern}')
results = await symsearch(pattern)
# print(f'results\n:{pformat(results)}')
if results:
# print(f"results: {results}")
# TODO: indented branch results for each provider
search.view.set_results(
[item['altname'] for item in
results['kraken'].values()]
)
# XXX: these 2 lines MUST be in sequence !?
sel.setCurrentIndex(
model.index(0, 0, QModelIndex()),
QItemSelectionModel.ClearAndSelect | # type: ignore[arg-type]
QItemSelectionModel.Rows
)
search.show()
async def handle_keyboard_input( async def handle_keyboard_input(
self, search: FontSizedQLineEdit,
recv_chan: trio.abc.ReceiveChannel, recv_chan: trio.abc.ReceiveChannel,
) -> None: ) -> None:
async for key, mods, txt in recv_chan: # startup
view = search.view
# by default we don't mart it as consumed? view.set_font_size(search.dpi_font.px_size)
# ev.ignore()
print(f'key: {key}, mods: {mods}, txt: {txt}')
if key in (Qt.Key_Enter, Qt.Key_Return):
print(f'Requesting symbol: {self.text()}')
# TODO: ensure there is a matching completion or error and
# do nothing
symbol = self.text()
app = self.chart_app
self.chart_app.load_symbol(
app.linkedcharts.symbol.brokers[0],
symbol,
'info',
)
# release kb control of search bar
self.unfocus()
continue
# return
ctrl = False
if mods == Qt.ControlModifier:
ctrl = True
view = self.view
model = view.model() model = view.model()
nidx = cidx = view.currentIndex() nidx = cidx = view.currentIndex()
sel = view.selectionModel() sel = view.selectionModel()
# sel.clear() # sel.clear()
symsearch = feed.get_multi_search()
send, recv = trio.open_memory_channel(16)
async with trio.open_nursery() as n:
# TODO: async debouncing!
n.start_soon(
fill_results,
search,
symsearch,
recv,
# pattern,
)
async for key, mods, txt in recv_chan:
# startup
# view = search.view
# view.set_font_size(search.dpi_font.px_size)
# model = view.model()
nidx = cidx = view.currentIndex()
# sel = view.selectionModel()
# by default we don't mart it as consumed?
# ev.ignore()
search.show()
log.debug(f'key: {key}, mods: {mods}, txt: {txt}')
ctrl = False
if mods == Qt.ControlModifier:
ctrl = True
if key in (Qt.Key_Enter, Qt.Key_Return):
value = model.item(nidx.row(), 2).text()
log.info(f'Requesting symbol: {value}')
app = search.chart_app
search.chart_app.load_symbol(
app.linkedcharts.symbol.brokers[0],
value,
'info',
)
# release kb control of search bar
search.unfocus()
continue
# selection tips: # selection tips:
# - get parent: self.index(row, 0) # - get parent: search.index(row, 0)
# - first item index: index = self.index(0, 0, parent) # - first item index: index = search.index(0, 0, parent)
if ctrl: if ctrl:
# we're in select mode or cancelling # we're in select mode or cancelling
if key == Qt.Key_C: if key == Qt.Key_C:
self.unfocus() search.unfocus()
# kill the search and focus back on main chart # kill the search and focus back on main chart
if self.chart_app: if search.chart_app:
self.chart_app.linkedcharts.focus() search.chart_app.linkedcharts.focus()
# return
continue continue
# result selection nav # result selection nav
if key in (Qt.Key_K, Qt.Key_J): if key in (Qt.Key_K, Qt.Key_J):
if key == Qt.Key_K: if key == Qt.Key_K:
# self.view.setFocus() # search.view.setFocus()
nidx = view.indexAbove(cidx) nidx = view.indexAbove(cidx)
print('move up') print('move up')
elif key == Qt.Key_J: elif key == Qt.Key_J:
# self.view.setFocus() # search.view.setFocus()
nidx = view.indexBelow(cidx) nidx = view.indexBelow(cidx)
print('move down') print('move down')
@ -393,16 +452,21 @@ async def handle_keyboard_input(
# to figure out the desired field(s) # to figure out the desired field(s)
value = model.item(nidx.row(), 2).text() value = model.item(nidx.row(), 2).text()
print(f'value: {value}') print(f'value: {value}')
self.setText(value) # search.setText(value)
# continue
else: else:
# auto-select the top matching result
sel.setCurrentIndex( sel.setCurrentIndex(
model.index(0, 0, QModelIndex()), model.index(0, 0, QModelIndex()),
QItemSelectionModel.ClearAndSelect | # type: ignore[arg-type] QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Rows QItemSelectionModel.Rows
) )
search.view.show_matches()
self.show_matches() else:
# relay to completer task
send.send_nowait(search.text())
if __name__ == '__main__': if __name__ == '__main__':
@ -413,13 +477,14 @@ if __name__ == '__main__':
syms = [ syms = [
'XMRUSD', 'XMRUSD',
'XBTUSD', 'XBTUSD',
'ETHUSD',
'XMRXBT', 'XMRXBT',
'XDGUSD', 'XDGUSD',
'ADAUSD', 'ADAUSD',
] ]
# results.setFocusPolicy(Qt.NoFocus) # results.setFocusPolicy(Qt.NoFocus)
view = mk_completer_view(['src', 'i', 'symbol']) view = CompleterView(['src', 'i', 'symbol'])
search = FontSizedQLineEdit(None, view=view) search = FontSizedQLineEdit(None, view=view)
search.view.set_results(syms) search.view.set_results(syms)
@ -434,7 +499,6 @@ if __name__ == '__main__':
main = W() main = W()
main.vbox.addWidget(search) main.vbox.addWidget(search)
main.vbox.addWidget(view) main.vbox.addWidget(view)
# main.show()
search.show() search.show()
sys.exit(app.exec_()) sys.exit(app.exec_())