Initial WIP search completer; still a mess

symbol_search
Tyler Goodlet 2021-04-23 11:12:29 -04:00
parent c26f4d9877
commit 0e83906f11
2 changed files with 435 additions and 113 deletions

View File

@ -24,7 +24,6 @@ from types import ModuleType
from functools import partial from functools import partial
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
from PyQt5 import QtWidgets
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
import tractor import tractor
@ -48,7 +47,6 @@ from ._graphics._ohlc import BarItems
from ._graphics._curve import FastAppendCurve from ._graphics._curve import FastAppendCurve
from ._style import ( from ._style import (
_font, _font,
DpiAwareFont,
hcolor, hcolor,
CHART_MARGINS, CHART_MARGINS,
_xaxis_at, _xaxis_at,
@ -56,6 +54,7 @@ from ._style import (
_bars_from_right_in_follow_mode, _bars_from_right_in_follow_mode,
_bars_to_left_in_follow_mode, _bars_to_left_in_follow_mode,
) )
from ._search import FontSizedQLineEdit
from ..data._source import Symbol from ..data._source import Symbol
from ..data._sharedmem import ShmArray from ..data._sharedmem import ShmArray
from .. import brokers from .. import brokers
@ -71,99 +70,6 @@ from .. import fsp
log = get_logger(__name__) log = get_logger(__name__)
class FontSizedQLineEdit(QtWidgets.QLineEdit):
def __init__(
self,
parent_chart: 'ChartSpace',
font: DpiAwareFont = _font,
) -> None:
super().__init__(parent_chart)
self.dpi_font = font
self.chart_app = parent_chart
# size it as we specify
self.setSizePolicy(
QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.Fixed,
)
self.setFont(font.font)
# witty bit of margin
self.setTextMargins(2, 2, 2, 2)
def sizeHint(self) -> QtCore.QSize:
psh = super().sizeHint()
psh.setHeight(self.dpi_font.px_size + 2)
return psh
def unfocus(self) -> None:
self.hide()
self.clearFocus()
def keyPressEvent(self, ev: QtCore.QEvent) -> None:
# by default we don't markt it as consumed?
# ev.ignore()
super().keyPressEvent(ev)
ev.accept()
# text = ev.text()
key = ev.key()
mods = ev.modifiers()
ctrl = False
if mods == QtCore.Qt.ControlModifier:
ctrl = True
if ctrl:
if key == QtCore.Qt.Key_C:
self.unfocus()
self.chart_app.linkedcharts.focus()
# TODO:
elif key == QtCore.Qt.Key_K:
print('move up')
elif key == QtCore.Qt.Key_J:
print('move down')
elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
print(f'Requesting symbol: {self.text()}')
symbol = self.text()
app = self.chart_app
self.chart_app.load_symbol(
app.linkedcharts.symbol.brokers[0],
symbol,
'info',
)
# self.hide()
self.unfocus()
# if self._executing():
# # ignore all key presses while executing, except for Ctrl-C
# if event.modifiers() == Qt.ControlModifier and key == Qt.Key_C:
# self._handle_ctrl_c()
# return True
# handler = self._key_event_handlers.get(key)
# intercepted = handler and handler(event)
# Assumes that Control+Key is a movement command, i.e. should not be
# handled as text insertion. However, on win10 AltGr is reported as
# Alt+Control which is why we handle this case like regular
# # keypresses, see #53:
# if not event.modifiers() & Qt.ControlModifier or \
# event.modifiers() & Qt.AltModifier:
# self._keep_cursor_in_buffer()
# if not intercepted and event.text():
# intercepted = True
# self.insert_input_text(event.text())
# return False
class ChartSpace(QtGui.QWidget): class ChartSpace(QtGui.QWidget):
"""High level widget which contains layouts for organizing """High level widget which contains layouts for organizing
lower level charts as well as other widgets used to control lower level charts as well as other widgets used to control
@ -172,9 +78,9 @@ class ChartSpace(QtGui.QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.v_layout = QtGui.QVBoxLayout(self) self.vbox = QtGui.QVBoxLayout(self)
self.v_layout.setContentsMargins(0, 0, 0, 0) self.vbox.setContentsMargins(0, 0, 0, 0)
self.v_layout.setSpacing(1) self.vbox.setSpacing(2)
self.toolbar_layout = QtGui.QHBoxLayout() self.toolbar_layout = QtGui.QHBoxLayout()
self.toolbar_layout.setContentsMargins(0, 0, 0, 0) self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
@ -184,8 +90,8 @@ class ChartSpace(QtGui.QWidget):
# self.init_timeframes_ui() # self.init_timeframes_ui()
# self.init_strategy_ui() # self.init_strategy_ui()
self.v_layout.addLayout(self.toolbar_layout) self.vbox.addLayout(self.toolbar_layout)
self.v_layout.addLayout(self.h_layout) self.vbox.addLayout(self.h_layout)
self._chart_cache = {} self._chart_cache = {}
self.linkedcharts: 'LinkedSplitCharts' = None self.linkedcharts: 'LinkedSplitCharts' = None
self.symbol_label: Optional[QtGui.QLabel] = None self.symbol_label: Optional[QtGui.QLabel] = None
@ -196,7 +102,16 @@ class ChartSpace(QtGui.QWidget):
# search = self.search = QtWidgets.QLineEdit() # search = self.search = QtWidgets.QLineEdit()
self.search = FontSizedQLineEdit(self) self.search = FontSizedQLineEdit(self)
self.search.unfocus() self.search.unfocus()
self.v_layout.addWidget(self.search) self.vbox.addWidget(self.search)
self.vbox.addWidget(self.search.view)
self.search.view.set_results([
'XMRUSD',
'XBTUSD',
'XMRXBT',
# 'XMRXBT',
# 'XDGUSD',
# 'ADAUSD',
])
# search.installEventFilter(self) # search.installEventFilter(self)
@ -237,14 +152,13 @@ class ChartSpace(QtGui.QWidget):
""" """
linkedcharts = self._chart_cache.get(symbol_key) linkedcharts = self._chart_cache.get(symbol_key)
# switching to a new viewable chart if not self.vbox.isEmpty():
if not self.v_layout.isEmpty():
# XXX: this is CRITICAL especially with pixel buffer caching # XXX: this is CRITICAL especially with pixel buffer caching
self.linkedcharts.hide() self.linkedcharts.hide()
# XXX: pretty sure we don't need this # XXX: pretty sure we don't need this
# remove any existing plots? # remove any existing plots?
# self.v_layout.removeWidget(self.linkedcharts) # self.vbox.removeWidget(self.linkedcharts)
# switching to a new viewable chart # switching to a new viewable chart
if linkedcharts is None or reset: if linkedcharts is None or reset:
@ -258,14 +172,14 @@ class ChartSpace(QtGui.QWidget):
symbol_key, symbol_key,
loglevel, loglevel,
) )
self.v_layout.addWidget(linkedcharts) self.vbox.addWidget(linkedcharts)
self._chart_cache[symbol_key] = linkedcharts self._chart_cache[symbol_key] = linkedcharts
# chart is already in memory so just focus it # chart is already in memory so just focus it
if self.linkedcharts: if self.linkedcharts:
self.linkedcharts.unfocus() self.linkedcharts.unfocus()
# self.v_layout.addWidget(linkedcharts) # self.vbox.addWidget(linkedcharts)
linkedcharts.show() linkedcharts.show()
linkedcharts.focus() linkedcharts.focus()
self.linkedcharts = linkedcharts self.linkedcharts = linkedcharts
@ -1348,7 +1262,6 @@ async def run_fsp(
# data-array as first msg # data-array as first msg
_ = await stream.receive() _ = await stream.receive()
conf['stream'] = stream
conf['portal'] = portal conf['portal'] = portal
shm = conf['shm'] shm = conf['shm']
@ -1414,8 +1327,6 @@ async def run_fsp(
chart._set_yrange() chart._set_yrange()
stream = conf['stream']
last = time.time() last = time.time()
# update chart graphics # update chart graphics
@ -1676,7 +1587,6 @@ async def _async_main(
# configure global DPI aware font size # configure global DPI aware font size
_font.configure_to_dpi(screen) _font.configure_to_dpi(screen)
# try:
async with trio.open_nursery() as root_n: async with trio.open_nursery() as root_n:
# set root nursery for spawning other charts/feeds # set root nursery for spawning other charts/feeds
@ -1691,9 +1601,6 @@ async def _async_main(
await trio.sleep_forever() await trio.sleep_forever()
# finally:
# root_n.cancel()
def _main( def _main(
sym: str, sym: str,

415
piker/ui/_search.py 100644
View File

@ -0,0 +1,415 @@
# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
qompleterz: embeddable search and complete using trio, Qt and fuzzywuzzy.
"""
from typing import Dict, List, Optional
import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt
from PyQt5 import QtWidgets
from PyQt5.QtCore import (
Qt,
QSize,
QModelIndex,
QItemSelectionModel,
# QStringListModel
)
from PyQt5.QtGui import (
QStandardItem,
QStandardItemModel,
)
from PyQt5.QtWidgets import (
QTreeView,
# QListWidgetItem,
QAbstractScrollArea,
QStyledItemDelegate,
)
from fuzzywuzzy import process
# from PyQt5.QtWidgets import QCompleter, QComboBox
from ..log import get_logger
from ._style import (
_font,
DpiAwareFont,
hcolor,
)
log = get_logger(__name__)
class SimpleDelegate(QStyledItemDelegate):
"""
Super simple view delegate to render text in the same
font size as the search widget.
"""
def __init__(
self,
parent=None,
font: DpiAwareFont = _font,
) -> None:
super().__init__(parent)
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):
def __init__(
self,
parent=None,
) -> None:
super().__init__(parent)
self._font_size: int = 0 # pixels
self._cache: Dict[str, List[str]] = {}
# def viewportSizeHint(self) -> QtCore.QSize:
# vps = super().viewportSizeHint()
# return QSize(vps.width(), _font.px_size * 6 * 2)
# def sizeHint(self) -> QtCore.QSize:
# """Scale completion results up to 6/16 of window.
# """
# # height = self.window().height() * 1/6
# # psh.setHeight(self.dpi_font.px_size * 6)
# # print(_font.px_size)
# height = _font.px_size * 6 * 2
# # the default here is just the vp size without scroll bar
# # https://doc.qt.io/qt-5/qabstractscrollarea.html#viewportSizeHint
# vps = self.viewportSizeHint()
# # print(f'h: {height}\n{vps}')
# # psh.setHeight(12)
# 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}')
self.setMinimumSize(self.width(), rows * row_px)
self.setMaximumSize(self.width(), rows * row_px)
def set_font_size(self, size: int = 18):
# dpi_px_size = _font.px_size
print(size)
if size < 0:
size = 16
self._font_size = size
self.setStyleSheet(f"font: {size}px")
def set_results(
self,
results: List[str],
) -> None:
model = self.model()
for i, s in enumerate(results):
ix = QStandardItem(str(i))
item = QStandardItem(s)
# item.setCheckable(False)
src = QStandardItem('kraken')
# Add the item to the model
model.appendRow([src, ix, item])
def find_matches(
self,
field: str,
txt: str,
) -> List[QStandardItem]:
model = self.model()
items = model.findItems(
txt,
Qt.MatchContains,
self.field_to_col(field),
)
def mk_completer_view(
labels: List[str],
) -> QTreeView:
tree = CompleterView()
model = QStandardItemModel(tree)
# a std "tabular" config
tree.setItemDelegate(SimpleDelegate())
tree.setModel(model)
tree.setAlternatingRowColors(True)
tree.setIndentation(1)
# 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):
def __init__(
self,
parent_chart: 'ChartSpace', # noqa
view: Optional[CompleterView] = None,
font: DpiAwareFont = _font,
) -> None:
super().__init__(parent_chart)
# vbox = self.vbox = QtGui.QVBoxLayout(self)
# vbox.addWidget(self)
# self.vbox.setContentsMargins(0, 0, 0, 0)
# self.vbox.setSpacing(2)
self._view: CompleterView = view
self.dpi_font = font
self.chart_app = parent_chart
# size it as we specify
self.setSizePolicy(
QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.Fixed,
)
self.setFont(font.font)
# witty bit of margin
self.setTextMargins(2, 2, 2, 2)
# self.setContextMenuPolicy(Qt.CustomContextMenu)
# self.customContextMenuRequested.connect(self.show_menu)
# self.setStyleSheet(f"font: 18px")
def focus(self) -> None:
self.clear()
self.show()
self.setFocus()
def show(self) -> None:
super().show()
self.show_matches()
# self.view.show()
# self.view.resize()
@property
def view(self) -> CompleterView:
if self._view is None:
view = mk_completer_view(['src', 'i', 'symbol'])
# print('yo')
# self.chart_app.vbox.addWidget(view)
# self.vbox.addWidget(view)
self._view = 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:
"""
Scale edit box to size of dpi aware font.
"""
psh = super().sizeHint()
psh.setHeight(self.dpi_font.px_size + 2)
# psh.setHeight(12)
return psh
def unfocus(self) -> None:
self.hide()
self.clearFocus()
if self.view:
self.view.hide()
def keyPressEvent(self, ev: QtCore.QEvent) -> None:
# by default we don't markt it as consumed?
# ev.ignore()
super().keyPressEvent(ev)
ev.accept()
# text = ev.text()
key = ev.key()
mods = ev.modifiers()
txt = self.text()
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 = txt
app = self.chart_app
self.chart_app.load_symbol(
app.linkedcharts.symbol.brokers[0],
symbol,
'info',
)
self.unfocus()
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
# 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(
model.index(0, 0, QModelIndex()),
QItemSelectionModel.ClearAndSelect | # type: ignore[arg-type]
QItemSelectionModel.Rows
)
self.show_matches()
if __name__ == '__main__':
# local testing of **just** the search UI
app = QtWidgets.QApplication(sys.argv)
syms = [
'XMRUSD',
'XBTUSD',
'XMRXBT',
'XDGUSD',
'ADAUSD',
]
# results.setFocusPolicy(Qt.NoFocus)
search = FontSizedQLineEdit(None, view=view)
search.view.set_results(syms)
# make a root widget to tie shit together
class W(QtGui.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
vbox = self.vbox = QtGui.QVBoxLayout(self)
self.vbox.setContentsMargins(0, 0, 0, 0)
self.vbox.setSpacing(2)
main = W()
main.vbox.addWidget(search)
main.vbox.addWidget(view)
# main.show()
search.show()
sys.exit(app.exec_())