Add initial working symbol search with async completions
parent
ad494db213
commit
431fdd3f9c
|
@ -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,102 +310,163 @@ class FontSizedQLineEdit(QtWidgets.QLineEdit):
|
||||||
# # ev.accept()
|
# # ev.accept()
|
||||||
|
|
||||||
|
|
||||||
async def handle_keyboard_input(
|
async def fill_results(
|
||||||
self,
|
search: FontSizedQLineEdit,
|
||||||
|
symsearch: Callable[..., Awaitable],
|
||||||
recv_chan: trio.abc.ReceiveChannel,
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
|
# pattern: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
async for key, mods, txt in recv_chan:
|
sel = search.view.selectionModel()
|
||||||
|
model = search.view.model()
|
||||||
|
|
||||||
# by default we don't mart it as consumed?
|
async for pattern in recv_chan:
|
||||||
# ev.ignore()
|
# so symbol search
|
||||||
|
# pattern = search.text()
|
||||||
|
# print(f'searching for: {pattern}')
|
||||||
|
|
||||||
print(f'key: {key}, mods: {mods}, txt: {txt}')
|
results = await symsearch(pattern)
|
||||||
|
# print(f'results\n:{pformat(results)}')
|
||||||
|
|
||||||
if key in (Qt.Key_Enter, Qt.Key_Return):
|
if results:
|
||||||
|
# print(f"results: {results}")
|
||||||
|
|
||||||
print(f'Requesting symbol: {self.text()}')
|
# TODO: indented branch results for each provider
|
||||||
|
search.view.set_results(
|
||||||
# TODO: ensure there is a matching completion or error and
|
[item['altname'] for item in
|
||||||
# do nothing
|
results['kraken'].values()]
|
||||||
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
|
# XXX: these 2 lines MUST be in sequence !?
|
||||||
self.unfocus()
|
|
||||||
continue
|
|
||||||
# return
|
|
||||||
|
|
||||||
ctrl = False
|
|
||||||
if mods == Qt.ControlModifier:
|
|
||||||
ctrl = True
|
|
||||||
|
|
||||||
view = self.view
|
|
||||||
model = view.model()
|
|
||||||
nidx = cidx = view.currentIndex()
|
|
||||||
sel = view.selectionModel()
|
|
||||||
# sel.clear()
|
|
||||||
|
|
||||||
# selection tips:
|
|
||||||
# - get parent: self.index(row, 0)
|
|
||||||
# - first item index: index = self.index(0, 0, parent)
|
|
||||||
|
|
||||||
if ctrl:
|
|
||||||
# we're in select mode or cancelling
|
|
||||||
|
|
||||||
if key == Qt.Key_C:
|
|
||||||
self.unfocus()
|
|
||||||
|
|
||||||
# kill the search and focus back on main chart
|
|
||||||
if self.chart_app:
|
|
||||||
self.chart_app.linkedcharts.focus()
|
|
||||||
|
|
||||||
# return
|
|
||||||
continue
|
|
||||||
|
|
||||||
# result selection nav
|
|
||||||
if key in (Qt.Key_K, Qt.Key_J):
|
|
||||||
|
|
||||||
if key == Qt.Key_K:
|
|
||||||
# self.view.setFocus()
|
|
||||||
nidx = view.indexAbove(cidx)
|
|
||||||
print('move up')
|
|
||||||
|
|
||||||
elif key == Qt.Key_J:
|
|
||||||
# self.view.setFocus()
|
|
||||||
nidx = view.indexBelow(cidx)
|
|
||||||
print('move down')
|
|
||||||
|
|
||||||
# select row without selecting.. :eye_rollzz:
|
|
||||||
# https://doc.qt.io/qt-5/qabstractitemview.html#setCurrentIndex
|
|
||||||
if nidx.isValid():
|
|
||||||
sel.setCurrentIndex(
|
|
||||||
nidx,
|
|
||||||
QItemSelectionModel.ClearAndSelect |
|
|
||||||
QItemSelectionModel.Rows
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: make this not hard coded to 2
|
|
||||||
# and use the ``CompleterView`` schema/settings
|
|
||||||
# to figure out the desired field(s)
|
|
||||||
value = model.item(nidx.row(), 2).text()
|
|
||||||
print(f'value: {value}')
|
|
||||||
self.setText(value)
|
|
||||||
|
|
||||||
else:
|
|
||||||
sel.setCurrentIndex(
|
sel.setCurrentIndex(
|
||||||
model.index(0, 0, QModelIndex()),
|
model.index(0, 0, QModelIndex()),
|
||||||
QItemSelectionModel.ClearAndSelect | # type: ignore[arg-type]
|
QItemSelectionModel.ClearAndSelect | # type: ignore[arg-type]
|
||||||
QItemSelectionModel.Rows
|
QItemSelectionModel.Rows
|
||||||
)
|
)
|
||||||
|
search.show()
|
||||||
|
|
||||||
self.show_matches()
|
|
||||||
|
async def handle_keyboard_input(
|
||||||
|
search: FontSizedQLineEdit,
|
||||||
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
# startup
|
||||||
|
view = search.view
|
||||||
|
view.set_font_size(search.dpi_font.px_size)
|
||||||
|
model = view.model()
|
||||||
|
nidx = cidx = view.currentIndex()
|
||||||
|
sel = view.selectionModel()
|
||||||
|
# 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:
|
||||||
|
# - get parent: search.index(row, 0)
|
||||||
|
# - first item index: index = search.index(0, 0, parent)
|
||||||
|
|
||||||
|
if ctrl:
|
||||||
|
# we're in select mode or cancelling
|
||||||
|
|
||||||
|
if key == Qt.Key_C:
|
||||||
|
search.unfocus()
|
||||||
|
|
||||||
|
# kill the search and focus back on main chart
|
||||||
|
if search.chart_app:
|
||||||
|
search.chart_app.linkedcharts.focus()
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
# result selection nav
|
||||||
|
if key in (Qt.Key_K, Qt.Key_J):
|
||||||
|
|
||||||
|
if key == Qt.Key_K:
|
||||||
|
# search.view.setFocus()
|
||||||
|
nidx = view.indexAbove(cidx)
|
||||||
|
print('move up')
|
||||||
|
|
||||||
|
elif key == Qt.Key_J:
|
||||||
|
# search.view.setFocus()
|
||||||
|
nidx = view.indexBelow(cidx)
|
||||||
|
print('move down')
|
||||||
|
|
||||||
|
# select row without selecting.. :eye_rollzz:
|
||||||
|
# https://doc.qt.io/qt-5/qabstractitemview.html#setCurrentIndex
|
||||||
|
if nidx.isValid():
|
||||||
|
sel.setCurrentIndex(
|
||||||
|
nidx,
|
||||||
|
QItemSelectionModel.ClearAndSelect |
|
||||||
|
QItemSelectionModel.Rows
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: make this not hard coded to 2
|
||||||
|
# and use the ``CompleterView`` schema/settings
|
||||||
|
# to figure out the desired field(s)
|
||||||
|
value = model.item(nidx.row(), 2).text()
|
||||||
|
print(f'value: {value}')
|
||||||
|
# search.setText(value)
|
||||||
|
# continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
# auto-select the top matching result
|
||||||
|
sel.setCurrentIndex(
|
||||||
|
model.index(0, 0, QModelIndex()),
|
||||||
|
QItemSelectionModel.ClearAndSelect |
|
||||||
|
QItemSelectionModel.Rows
|
||||||
|
)
|
||||||
|
search.view.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_())
|
||||||
|
|
Loading…
Reference in New Issue