From 8d66a17daf5f930abb0435a70539353c79eb350d Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 5 Jan 2021 13:37:03 -0500 Subject: [PATCH] Add a "lines editor" api/component --- piker/_ems.py | 14 ++- piker/ui/_graphics/_lines.py | 20 +++- piker/ui/_interaction.py | 223 ++++++++++++++++++++++------------- 3 files changed, 161 insertions(+), 96 deletions(-) diff --git a/piker/_ems.py b/piker/_ems.py index 52b55d97..93175e12 100644 --- a/piker/_ems.py +++ b/piker/_ems.py @@ -1,5 +1,5 @@ # piker: trading gear for hackers -# Copyright (C) 2018-present Tyler Goodlet (in stewardship for piker0) +# 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 @@ -47,8 +47,8 @@ _local_book = {} @dataclass -class OrderBook: - """Send (client?) side order book tracking. +class OrderBoi: + """'Buy' (client ?) side order book ctl and tracking. Mostly for keeping local state to match the EMS and use events to trigger graphics updates. @@ -73,14 +73,14 @@ class OrderBook: ... -_orders: OrderBook = None +_orders: OrderBoi = None -def get_orders() -> OrderBook: +def get_orders() -> OrderBoi: global _orders if _orders is None: - _orders = OrderBook + _orders = OrderBoi return _orders @@ -392,6 +392,8 @@ async def spawn_router_stream_alerts( oid = alert['oid'] print(f'_lines: {_lines}') print(f'deleting line with oid: {oid}') + + chart._vb._lines_editor _lines.pop(oid).delete() # TODO: this in another task? diff --git a/piker/ui/_graphics/_lines.py b/piker/ui/_graphics/_lines.py index ef4c00f1..c6bf1818 100644 --- a/piker/ui/_graphics/_lines.py +++ b/piker/ui/_graphics/_lines.py @@ -18,6 +18,7 @@ Lines for orders, alerts, L2. """ +from dataclasses import dataclass from typing import Tuple import pyqtgraph as pg @@ -87,10 +88,11 @@ class LevelLabel(YSticky): self.level = level def set_label_str(self, level: float): - # this is read inside ``.paint()`` # self.label_str = '{size} x {level:.{digits}f}'.format( - self.label_str = '{level:.{digits}f}'.format( # size=self._size, + + # this is read inside ``.paint()`` + self.label_str = '{level:.{digits}f}'.format( digits=self.digits, level=level ).replace(',', ' ') @@ -200,14 +202,16 @@ class LevelLine(pg.InfiniteLine): chart: 'ChartPlotWidget', # type: ignore # noqa label: LevelLabel, highlight_color: str = 'default_light', + hl_on_hover: bool = True, **kwargs, ) -> None: - self.label = label + super().__init__(**kwargs) + self.label = label self.sigPositionChanged.connect(self.set_level) - self._chart = chart + self._hoh = hl_on_hover # use slightly thicker highlight pen = pg.mkPen(hcolor(highlight_color)) @@ -231,7 +235,8 @@ class LevelLine(pg.InfiniteLine): """Mouse hover callback. """ - if self.mouseHovering == hover: + # XXX: currently we'll just return if _hoh is False + if self.mouseHovering == hover or not self._hoh: return self.mouseHovering = hover @@ -315,6 +320,10 @@ def level_line( show_label: bool = True, + # whether or not the line placed in view should highlight + # when moused over (aka "hovered") + hl_on_hover: bool = True, + **linelabelkwargs ) -> LevelLine: """Convenience routine to add a styled horizontal line to a plot. @@ -346,6 +355,7 @@ def level_line( highlight_color=color + '_light', movable=True, angle=0, + hl_on_hover=hl_on_hover, ) line.setValue(level) line.setPen(pg.mkPen(hcolor(color))) diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index e9ad8744..fdaeb137 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -1,5 +1,5 @@ # piker: trading gear for hackers -# Copyright (C) 2018-present Tyler Goodlet (in stewardship of piker0) +# 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 @@ -17,7 +17,7 @@ """ UX interaction customs. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional import uuid @@ -28,7 +28,7 @@ import numpy as np from ..log import get_logger from ._style import _min_points_to_show, hcolor, _font -from ._graphics._lines import level_line +from ._graphics._lines import level_line, LevelLine from .._ems import _lines @@ -199,19 +199,133 @@ class SelectRect(QtGui.QGraphicsRectItem): @dataclass -class LinesEditor: +class LineEditor: view: 'ChartView' - chart: 'ChartPlotWidget' - active_line: 'LevelLine' + _lines: field(default_factory=dict) + chart: 'ChartPlotWidget' = None # type: ignore # noqa + _active_staged_line: LevelLine = None + _stage_line: LevelLine = None - def stage_line(self) -> 'LevelLine': - ... + def stage_line(self, color: str = 'alert_yellow') -> LevelLine: + """Stage a line at the current chart's cursor position + and return it. - def commit_line(self) -> 'LevelLine': - ... + """ + chart = self.chart._cursor.active_plot + chart.setCursor(QtCore.Qt.PointingHandCursor) + cursor = chart._cursor + y = chart._cursor._datum_xy[1] - def remove_line(self, line) -> None: - ... + line = self._stage_line + if not line: + # add a "staged" cursor-tracking line to view + # and cash it in a a var + line = level_line( + chart, + level=y, + color=color, + + # don't highlight the "staging" line + hl_on_hover=False, + ) + self._stage_line = line + + else: + # use the existing staged line instead + # of allocating more mem / objects repeatedly + line.setValue(y) + line.show() + line.label.show() + + self._active_staged_line = line + + # hide crosshair y-line + cursor.graphics[chart]['hl'].hide() + + # add line to cursor trackers + cursor._trackers.add(line) + + return line + + def unstage_line(self) -> LevelLine: + """Inverse of ``.stage_line()``. + + """ + chart = self.chart._cursor.active_plot + chart.setCursor(QtCore.Qt.ArrowCursor) + cursor = chart._cursor + + # delete "staged" cursor tracking line from view + line = self._active_staged_line + + cursor._trackers.remove(line) + + if self._stage_line: + self._stage_line.hide() + self._stage_line.label.hide() + + # if line: + # line.delete() + + self._active_staged_line = None + + # show the crosshair y line + hl = cursor.graphics[chart]['hl'] + hl.show() + + def commit_line(self) -> LevelLine: + line = self._active_staged_line + if line: + chart = self.chart._cursor.active_plot + + y = chart._cursor._datum_xy[1] + + # XXX: should make this an explicit attr + # it's assigned inside ``.add_plot()`` + lc = self.view.linked_charts + + oid = str(uuid.uuid4()) + lc._to_router.send_nowait({ + 'chart': lc, + 'type': 'alert', + 'price': y, + 'oid': oid, + # 'symbol': lc.chart.name, + # 'brokers': lc.symbol.brokers, + # 'price': y, + }) + + line = level_line( + chart, + level=y, + color='alert_yellow', + ) + # register for later + _lines[oid] = line + + log.debug(f'clicked y: {y}') + + def remove_line( + self, + line: LevelLine = None, + uuid: str = None, + ) -> None: + """Remove a line by refernce or uuid. + + If no lines or ids are provided remove all lines under the + cursor position. + + """ + # Delete any hoverable under the cursor + cursor = self.chart._cursor + + if line: + line.delete() + else: + for item in cursor._hovered: + # hovered items must also offer + # a ``.delete()`` method + item.delete() class ChartView(ViewBox): @@ -236,8 +350,9 @@ class ChartView(ViewBox): self.addItem(self.select_box, ignoreBounds=True) self._chart: 'ChartPlotWidget' = None # noqa + self._lines_editor = LineEditor(view=self, _lines=_lines) self._key_buffer = [] - self._active_staged_line: 'LevelLine' = None # noqa + self._active_staged_line: LevelLine = None # noqa @property def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa @@ -247,6 +362,7 @@ class ChartView(ViewBox): def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa self._chart = chart self.select_box.chart = chart + self._lines_editor.chart = chart def wheelEvent(self, ev, axis=None): """Override "center-point" location for scrolling. @@ -407,7 +523,7 @@ class ChartView(ViewBox): """ button = ev.button() - pos = ev.pos() + # pos = ev.pos() if button == QtCore.Qt.RightButton and self.menuEnabled(): ev.accept() @@ -417,33 +533,8 @@ class ChartView(ViewBox): ev.accept() - line = self._active_staged_line - if line: - chart = self.chart._cursor.active_plot - - y = chart._cursor._datum_xy[1] - - # XXX: should make this an explicit attr - # it's assigned inside ``.add_plot()`` - lc = self.linked_charts - oid = str(uuid.uuid4()) - lc._to_router.send_nowait({ - 'chart': lc, - 'type': 'alert', - 'price': y, - 'oid': oid, - # 'symbol': lc.chart.name, - # 'brokers': lc.symbol.brokers, - # 'price': y, - }) - - line = level_line( - chart, - level=y, - color='alert_yellow', - ) - _lines[oid] = line - log.info(f'clicked {pos}') + # commit the "staged" line under the cursor + self._lines_editor.commit_line() def keyReleaseEvent(self, ev): """ @@ -465,23 +556,8 @@ class ChartView(ViewBox): self.setMouseMode(ViewBox.PanMode) if text == 'a': - - chart = self.chart._cursor.active_plot - chart.setCursor(QtCore.Qt.ArrowCursor) - cursor = chart._cursor - - # delete "staged" cursor tracking line from view - line = self._active_staged_line - cursor._trackers.remove(line) - - if line: - line.delete() - - self._active_staged_line = None - - # show the crosshair y line - hl = cursor.graphics[chart]['hl'] - hl.show() + # draw "staged" line under cursor position + self._lines_editor.unstage_line() def keyPressEvent(self, ev): """ @@ -526,35 +602,12 @@ class ChartView(ViewBox): self.chart.default_view() elif text == 'a': - - chart = self.chart._cursor.active_plot - chart.setCursor(QtCore.Qt.PointingHandCursor) - cursor = chart._cursor - - # add a "staged" cursor-tracking alert line - - line = level_line( - chart, - level=chart._cursor._datum_xy[1], - color='alert_yellow', - ) - self._active_staged_line = line - - # hide crosshair y-line - cursor.graphics[chart]['hl'].hide() - - # add line to cursor trackers - cursor._trackers.add(line) + # add a line at the current cursor + self._lines_editor.stage_line() elif text == 'd': - # Delete any hoverable under the cursor - cursor = self.chart._cursor - chart = cursor.active_plot - - for item in cursor._hovered: - # hovered items must also offer - # a ``.delete()`` method - item.delete() + # delete any lines under the cursor + self._lines_editor.remove_line() # Leaving this for light reference purposes