Convert view box to async input handling

Instead of callbacks for key presses/releases convert our `ChartView`'s
kb input handling to async code using our event relaying-over-mem-chan
system. This is a first step toward a more async driven modal control
UX. Changed a bunch of "chart" component naming as part of this as well,
namely: `ChartSpace` -> `GodWidget` and `LinkedSplitCharts` ->
`LinkedSplits`. Engage the view boxe's async handler code as part of new
symbol data loading in `display_symbol_data()`. More re-orging to come!
asyncify_input_modes
Tyler Goodlet 2021-06-15 18:19:59 -04:00
parent 85621af8af
commit 75804a441c
3 changed files with 306 additions and 219 deletions

View File

@ -26,6 +26,7 @@ from functools import partial
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QEvent
import numpy as np
import pyqtgraph as pg
import tractor
@ -60,7 +61,7 @@ from ._style import (
_bars_to_left_in_follow_mode,
)
from . import _search
from ._event import open_key_stream
from . import _event
from ..data._source import Symbol
from ..data._sharedmem import ShmArray
from ..data import maybe_open_shm_array
@ -77,13 +78,23 @@ from ..data import feed
log = get_logger(__name__)
class ChartSpace(QtGui.QWidget):
'''Highest level composed widget which contains layouts for
class GodWidget(QtGui.QWidget):
'''
"Our lord and savior, the holy child of window-shua, there is no
widget above thee." - 6|6
The highest level composed widget which contains layouts for
organizing lower level charts as well as other widgets used to
control or modify them.
'''
def __init__(self, parent=None):
def __init__(
self,
parent=None,
) -> None:
super().__init__(parent)
self.hbox = QtGui.QHBoxLayout(self)
@ -96,51 +107,54 @@ class ChartSpace(QtGui.QWidget):
self.hbox.addLayout(self.vbox)
self.toolbar_layout = QtGui.QHBoxLayout()
self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
# self.toolbar_layout = QtGui.QHBoxLayout()
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
# self.vbox.addLayout(self.toolbar_layout)
# self.init_timeframes_ui()
# self.init_strategy_ui()
self.vbox.addLayout(self.toolbar_layout)
# self.vbox.addLayout(self.hbox)
self._chart_cache = {}
self.linkedcharts: 'LinkedSplitCharts' = None
self._root_n: Optional[trio.Nursery] = None
self.linkedsplits: 'LinkedSplits' = None
# assigned in the startup func `_async_main()`
self._root_n: trio.Nursery = None
self._task_stack: AsyncExitStack = None
def set_chart_symbol(
self,
symbol_key: str, # of form <fqsn>.<providername>
linked_charts: 'LinkedSplitCharts', # type: ignore
linkedsplits: 'LinkedSplits', # type: ignore
) -> None:
# re-sort org cache symbol list in LIFO order
cache = self._chart_cache
cache.pop(symbol_key, None)
cache[symbol_key] = linked_charts
cache[symbol_key] = linkedsplits
def get_chart_symbol(
self,
symbol_key: str,
) -> 'LinkedSplitCharts': # type: ignore
) -> 'LinkedSplits': # type: ignore
return self._chart_cache.get(symbol_key)
def init_timeframes_ui(self):
self.tf_layout = QtGui.QHBoxLayout()
self.tf_layout.setSpacing(0)
self.tf_layout.setContentsMargins(0, 12, 0, 0)
time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
btn_prefix = 'TF'
# def init_timeframes_ui(self):
# self.tf_layout = QtGui.QHBoxLayout()
# self.tf_layout.setSpacing(0)
# self.tf_layout.setContentsMargins(0, 12, 0, 0)
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
# btn_prefix = 'TF'
for tf in time_frames:
btn_name = ''.join([btn_prefix, tf])
btn = QtGui.QPushButton(tf)
# TODO:
btn.setEnabled(False)
setattr(self, btn_name, btn)
self.tf_layout.addWidget(btn)
# for tf in time_frames:
# btn_name = ''.join([btn_prefix, tf])
# btn = QtGui.QPushButton(tf)
# # TODO:
# btn.setEnabled(False)
# setattr(self, btn_name, btn)
# self.tf_layout.addWidget(btn)
self.toolbar_layout.addLayout(self.tf_layout)
# self.toolbar_layout.addLayout(self.tf_layout)
# XXX: strat loader/saver that we don't need yet.
# def init_strategy_ui(self):
@ -166,46 +180,46 @@ class ChartSpace(QtGui.QWidget):
# fully qualified symbol name (SNS i guess is what we're making?)
fqsn = '.'.join([symbol_key, providername])
linkedcharts = self.get_chart_symbol(fqsn)
linkedsplits = self.get_chart_symbol(fqsn)
if not self.vbox.isEmpty():
# XXX: this is CRITICAL especially with pixel buffer caching
self.linkedcharts.hide()
self.linkedsplits.hide()
# XXX: pretty sure we don't need this
# remove any existing plots?
# XXX: ahh we might want to support cache unloading..
self.vbox.removeWidget(self.linkedcharts)
self.vbox.removeWidget(self.linkedsplits)
# switching to a new viewable chart
if linkedcharts is None or reset:
if linkedsplits is None or reset:
# we must load a fresh linked charts set
linkedcharts = LinkedSplitCharts(self)
linkedsplits = LinkedSplits(self)
# spawn new task to start up and update new sub-chart instances
self._root_n.start_soon(
chart_symbol,
display_symbol_data,
self,
providername,
symbol_key,
loglevel,
)
self.set_chart_symbol(fqsn, linkedcharts)
self.set_chart_symbol(fqsn, linkedsplits)
self.vbox.addWidget(linkedcharts)
self.vbox.addWidget(linkedsplits)
# chart is already in memory so just focus it
if self.linkedcharts:
self.linkedcharts.unfocus()
if self.linkedsplits:
self.linkedsplits.unfocus()
# self.vbox.addWidget(linkedcharts)
linkedcharts.show()
linkedcharts.focus()
self.linkedcharts = linkedcharts
# self.vbox.addWidget(linkedsplits)
linkedsplits.show()
linkedsplits.focus()
self.linkedsplits = linkedsplits
symbol = linkedcharts.symbol
symbol = linkedsplits.symbol
if symbol is not None:
self.window.setWindowTitle(
@ -214,14 +228,16 @@ class ChartSpace(QtGui.QWidget):
)
class LinkedSplitCharts(QtGui.QWidget):
"""Widget that holds a central chart plus derived
class LinkedSplits(QtGui.QWidget):
'''
Widget that holds a central chart plus derived
subcharts computed from the original data set apart
by splitters for resizing.
A single internal references to the data is maintained
for each chart and can be updated externally.
"""
'''
long_pen = pg.mkPen('#006000')
long_brush = pg.mkBrush('#00ff00')
short_pen = pg.mkPen('#600000')
@ -232,21 +248,24 @@ class LinkedSplitCharts(QtGui.QWidget):
def __init__(
self,
chart_space: ChartSpace,
godwidget: GodWidget,
) -> None:
super().__init__()
self.signals_visible: bool = False
# self.signals_visible: bool = False
self._cursor: Cursor = None # crosshair graphics
self.godwidget = godwidget
self.chart: ChartPlotWidget = None # main (ohlc) chart
self.subplots: Dict[Tuple[str, ...], ChartPlotWidget] = {}
self.chart_space = chart_space
self.chart_space = chart_space
self.godwidget = godwidget
self.xaxis = DynamicDateAxis(
orientation='bottom',
linked_charts=self
linkedsplits=self
)
# if _xaxis_at == 'bottom':
# self.xaxis.setStyle(showValues=False)
@ -302,7 +321,7 @@ class LinkedSplitCharts(QtGui.QWidget):
"""
# add crosshairs
self._cursor = Cursor(
linkedsplitcharts=self,
linkedsplits=self,
digits=symbol.digits(),
)
self.chart = self.add_plot(
@ -342,14 +361,14 @@ class LinkedSplitCharts(QtGui.QWidget):
"A main plot must be created first with `.plot_ohlc_main()`")
# source of our custom interactions
cv = ChartView()
cv.linked_charts = self
cv = ChartView(name)
cv.linkedsplits = self
# use "indicator axis" by default
if xaxis is None:
xaxis = DynamicDateAxis(
orientation='bottom',
linked_charts=self
linkedsplits=self
)
cpw = ChartPlotWidget(
@ -360,11 +379,11 @@ class LinkedSplitCharts(QtGui.QWidget):
array=array,
parent=self.splitter,
linked_charts=self,
linkedsplits=self,
axisItems={
'bottom': xaxis,
'right': PriceAxis(linked_charts=self, orientation='right'),
'left': PriceAxis(linked_charts=self, orientation='left'),
'right': PriceAxis(linkedsplits=self, orientation='right'),
'left': PriceAxis(linkedsplits=self, orientation='left'),
},
viewBox=cv,
cursor=self._cursor,
@ -377,7 +396,7 @@ class LinkedSplitCharts(QtGui.QWidget):
# (see our custom view mode in `._interactions.py`)
cv.chart = cpw
cpw.plotItem.vb.linked_charts = self
cpw.plotItem.vb.linkedsplits = self
cpw.setFrameStyle(QtGui.QFrame.StyledPanel) # | QtGui.QFrame.Plain)
cpw.hideButtons()
# XXX: gives us outline on backside of y-axis
@ -415,7 +434,8 @@ class LinkedSplitCharts(QtGui.QWidget):
class ChartPlotWidget(pg.PlotWidget):
"""``GraphicsView`` subtype containing a single ``PlotItem``.
'''
``GraphicsView`` subtype containing a single ``PlotItem``.
- The added methods allow for plotting OHLC sequences from
``np.ndarray``s with appropriate field names.
@ -425,7 +445,8 @@ class ChartPlotWidget(pg.PlotWidget):
(Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
eventually want multiple plots managed together?)
"""
'''
sig_mouse_leave = QtCore.Signal(object)
sig_mouse_enter = QtCore.Signal(object)
@ -441,7 +462,7 @@ class ChartPlotWidget(pg.PlotWidget):
# the data view we generate graphics from
name: str,
array: np.ndarray,
linked_charts: LinkedSplitCharts,
linkedsplits: LinkedSplits,
view_color: str = 'papas_special',
pen_color: str = 'bracket',
@ -465,7 +486,7 @@ class ChartPlotWidget(pg.PlotWidget):
**kwargs
)
self.name = name
self._lc = linked_charts
self._lc = linkedsplits
# scene-local placeholder for book graphics
# sizing to avoid overlap with data contents
@ -1006,10 +1027,12 @@ _book_throttle_rate: int = 16 # Hz
async def chart_from_quotes(
chart: ChartPlotWidget,
stream,
stream: tractor.MsgStream,
ohlcv: np.ndarray,
wap_in_history: bool = False,
) -> None:
"""The 'main' (price) chart real-time update loop.
@ -1211,12 +1234,14 @@ async def chart_from_quotes(
async def spawn_fsps(
linked_charts: LinkedSplitCharts,
linkedsplits: LinkedSplits,
fsps: Dict[str, str],
sym,
src_shm,
brokermod,
loglevel,
) -> None:
"""Start financial signal processing in subactor.
@ -1224,7 +1249,7 @@ async def spawn_fsps(
"""
linked_charts.focus()
linkedsplits.focus()
# spawns sub-processes which execute cpu bound FSP code
async with tractor.open_nursery(loglevel=loglevel) as n:
@ -1271,7 +1296,7 @@ async def spawn_fsps(
ln.start_soon(
run_fsp,
portal,
linked_charts,
linkedsplits,
brokermod,
sym,
src_shm,
@ -1286,7 +1311,7 @@ async def spawn_fsps(
async def run_fsp(
portal: tractor._portal.Portal,
linked_charts: LinkedSplitCharts,
linkedsplits: LinkedSplits,
brokermod: ModuleType,
sym: str,
src_shm: ShmArray,
@ -1300,7 +1325,7 @@ async def run_fsp(
This is called once for each entry in the fsp
config map.
"""
done = linked_charts.window().status_bar.open_status(
done = linkedsplits.window().status_bar.open_status(
f'loading FSP: {display_name}..')
async with portal.open_stream_from(
@ -1324,7 +1349,7 @@ async def run_fsp(
shm = conf['shm']
if conf.get('overlay'):
chart = linked_charts.chart
chart = linkedsplits.chart
chart.draw_curve(
name='vwap',
data=shm.array,
@ -1334,7 +1359,7 @@ async def run_fsp(
else:
chart = linked_charts.add_plot(
chart = linkedsplits.add_plot(
name=fsp_func_name,
array=shm.array,
@ -1358,7 +1383,7 @@ async def run_fsp(
chart._shm = shm
# should **not** be the same sub-chart widget
assert chart.name != linked_charts.chart.name
assert chart.name != linkedsplits.chart.name
# sticky only on sub-charts atm
last_val_sticky = chart._ysticks[chart.name]
@ -1441,7 +1466,7 @@ async def run_fsp(
last = now
async def check_for_new_bars(feed, ohlcv, linked_charts):
async def check_for_new_bars(feed, ohlcv, linkedsplits):
"""Task which updates from new bars in the shared ohlcv buffer every
``delay_s`` seconds.
@ -1451,7 +1476,7 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
# Likely the best way to solve this is to make this task
# aware of the instrument's tradable hours?
price_chart = linked_charts.chart
price_chart = linkedsplits.chart
price_chart.default_view()
async with feed.index_stream() as stream:
@ -1489,33 +1514,37 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
price_chart._arrays[name]
)
for name, chart in linked_charts.subplots.items():
for name, chart in linkedsplits.subplots.items():
chart.update_curve_from_array(chart.name, chart._shm.array)
# shift the view if in follow mode
price_chart.increment_view()
async def chart_symbol(
chart_app: ChartSpace,
async def display_symbol_data(
godwidget: GodWidget,
provider: str,
sym: str,
loglevel: str,
) -> None:
"""Spawn a real-time chart widget for this symbol and app session.
'''Spawn a real-time displayed and updated chart for provider symbol.
These widgets can remain up but hidden so that multiple symbols
can be viewed and switched between extremely fast.
Spawned ``LinkedSplits`` chart widgets can remain up but hidden so
that multiple symbols can be viewed and switched between extremely
fast from a cached watch-list.
"""
sbar = chart_app.window.status_bar
'''
sbar = godwidget.window.status_bar
loading_sym_done = sbar.open_status(f'loading {sym}.{provider}..')
# historical data fetch
brokermod = brokers.get_brokermod(provider)
async with data.open_feed(
async with(
data.open_feed(
provider,
[sym],
loglevel=loglevel,
@ -1523,21 +1552,25 @@ async def chart_symbol(
# 60 FPS to limit context switches
tick_throttle=_clear_throttle_rate,
) as feed:
) as feed,
trio.open_nursery() as n,
):
ohlcv: ShmArray = feed.shm
bars = ohlcv.array
symbol = feed.symbols[sym]
# load in symbol's ohlc data
chart_app.window.setWindowTitle(
godwidget.window.setWindowTitle(
f'{symbol.key}@{symbol.brokers} '
f'tick:{symbol.tick_size}'
)
linked_charts = chart_app.linkedcharts
linked_charts._symbol = symbol
chart = linked_charts.plot_ohlc_main(symbol, bars)
linkedsplits = godwidget.linkedsplits
linkedsplits._symbol = symbol
chart = linkedsplits.plot_ohlc_main(symbol, bars)
chart.setFocus()
# plot historical vwap if available
@ -1591,12 +1624,11 @@ async def chart_symbol(
},
})
async with trio.open_nursery() as n:
# load initial fsp chain (otherwise known as "indicators")
n.start_soon(
spawn_fsps,
linked_charts,
linkedsplits,
fsp_conf,
sym,
ohlcv,
@ -1622,7 +1654,7 @@ async def chart_symbol(
feed,
# delay,
ohlcv,
linked_charts
linkedsplits
)
# interactive testing
@ -1630,15 +1662,21 @@ async def chart_symbol(
# test_bed,
# ohlcv,
# chart,
# linked_charts,
# linkedsplits,
# )
# start async input handling for chart's view
# await godwidget._task_stack.enter_async_context(
async with chart._vb.open_async_input_handler():
await start_order_mode(chart, symbol, provider)
async def load_providers(
brokernames: list[str],
loglevel: str,
) -> None:
# TODO: seems like our incentive for brokerd caching lelel
@ -1673,8 +1711,9 @@ async def load_providers(
async def _async_main(
# implicit required argument provided by ``qtractor_run()``
main_widget: ChartSpace,
main_widget: GodWidget,
sym: str,
brokernames: str,
@ -1688,13 +1727,14 @@ async def _async_main(
"""
chart_app = main_widget
godwidget = main_widget
# attempt to configure DPI aware font size
screen = chart_app.window.current_screen()
screen = godwidget.window.current_screen()
# configure graphics update throttling based on display refresh rate
global _clear_throttle_rate
_clear_throttle_rate = min(
round(screen.refreshRate()),
_clear_throttle_rate,
@ -1702,37 +1742,39 @@ async def _async_main(
log.info(f'Set graphics update rate to {_clear_throttle_rate} Hz')
# TODO: do styling / themeing setup
# _style.style_ze_sheets(chart_app)
# _style.style_ze_sheets(godwidget)
sbar = chart_app.window.status_bar
sbar = godwidget.window.status_bar
starting_done = sbar.open_status('starting ze chartz...')
async with trio.open_nursery() as root_n:
async with (
trio.open_nursery() as root_n,
AsyncExitStack() as chart_task_stack,
):
# set root nursery for spawning other charts/feeds
# set root nursery and task stack for spawning other charts/feeds
# that run cached in the bg
chart_app._root_n = root_n
godwidget._root_n = root_n
godwidget._task_stack = chart_task_stack
# setup search widget
search = _search.SearchWidget(chart_space=chart_app)
# the main chart's view is given focus at startup
# setup search widget and focus main chart view at startup
search = _search.SearchWidget(godwidget=godwidget)
search.bar.unfocus()
# add search singleton to global chart-space widget
chart_app.hbox.addWidget(
godwidget.hbox.addWidget(
search,
# alights to top and uses minmial space based on
# search bar size hint (i think?)
alignment=Qt.AlignTop
)
chart_app.search = search
godwidget.search = search
symbol, _, provider = sym.rpartition('.')
# this internally starts a ``chart_symbol()`` task above
chart_app.load_symbol(provider, symbol, loglevel)
# this internally starts a ``display_symbol_data()`` task above
godwidget.load_symbol(provider, symbol, loglevel)
# spin up a search engine for the local cached symbol set
async with _search.register_symbol_search(
@ -1740,7 +1782,7 @@ async def _async_main(
provider_name='cache',
search_routine=partial(
_search.search_simple_dict,
source=chart_app._chart_cache,
source=godwidget._chart_cache,
),
# cache is super fast so debounce on super short period
pause_period=0.01,
@ -1751,18 +1793,17 @@ async def _async_main(
root_n.start_soon(load_providers, brokernames, loglevel)
# start handling search bar kb inputs
async with open_key_stream(
async with (
_event.open_handler(
search.bar,
) as key_stream:
# start kb handling task for searcher
root_n.start_soon(
_search.handle_keyboard_input,
# chart_app,
search,
key_stream,
event_types={QEvent.KeyPress},
async_handler=_search.handle_keyboard_input,
# let key repeats pass through for search
filter_auto_repeats=False,
)
):
# remove startup status text
starting_done()
await trio.sleep_forever()
@ -1780,6 +1821,6 @@ def _main(
run_qtractor(
func=_async_main,
args=(sym, brokernames, piker_loglevel),
main_widget=ChartSpace,
main_widget=GodWidget,
tractor_kwargs=tractor_kwargs,
)

View File

@ -18,14 +18,17 @@
Chart view box primitives
"""
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import Optional, Dict
import pyqtgraph as pg
from PyQt5.QtCore import QPointF
from PyQt5.QtCore import QPointF, Qt
from PyQt5.QtCore import QEvent
from pyqtgraph import ViewBox, Point, QtCore, QtGui
from pyqtgraph import functions as fn
import numpy as np
import trio
from ..log import get_logger
from ._style import _min_points_to_show, hcolor, _font
@ -457,8 +460,28 @@ class ArrowEditor:
self.chart.plotItem.removeItem(arrow)
async def handle_viewmode_inputs(
view: 'ChartView',
recv_chan: trio.abc.ReceiveChannel,
) -> None:
async for event, etype, key, mods, text in recv_chan:
log.debug(f'key: {key}, mods: {mods}, text: {text}')
if etype in {QEvent.KeyPress}:
await view.on_key_press(text, key, mods)
elif etype in {QEvent.KeyRelease}:
await view.on_key_release(text, key, mods)
class ChartView(ViewBox):
"""Price chart view box with interaction behaviors you'd expect from
'''
Price chart view box with interaction behaviors you'd expect from
any interactive platform:
- zoom on mouse scroll that auto fits y-axis
@ -466,23 +489,30 @@ class ChartView(ViewBox):
- zoom on x to most recent in view datum
- zoom on right-click-n-drag to cursor position
"""
'''
mode_name: str = 'mode: view'
def __init__(
self,
name: str,
parent: pg.PlotItem = None,
**kwargs,
):
super().__init__(parent=parent, **kwargs)
# disable vertical scrolling
self.setMouseEnabled(x=True, y=False)
self.linked_charts = None
self.select_box = SelectRect(self)
self.addItem(self.select_box, ignoreBounds=True)
self.linkedsplits = None
self._chart: 'ChartPlotWidget' = None # noqa
# add our selection box annotator
self.select_box = SelectRect(self)
self.addItem(self.select_box, ignoreBounds=True)
self.name = name
self.mode = None
# kb ctrls processing
@ -491,6 +521,19 @@ class ChartView(ViewBox):
self.setFocusPolicy(QtCore.Qt.StrongFocus)
@asynccontextmanager
async def open_async_input_handler(
self,
) -> 'ChartView':
from . import _event
async with _event.open_handler(
self,
event_types={QEvent.KeyPress, QEvent.KeyRelease},
async_handler=handle_viewmode_inputs,
):
yield self
@property
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
return self._chart
@ -501,21 +544,21 @@ class ChartView(ViewBox):
self.select_box.chart = chart
def wheelEvent(self, ev, axis=None):
"""Override "center-point" location for scrolling.
'''Override "center-point" location for scrolling.
This is an override of the ``ViewBox`` method simply changing
the center of the zoom to be the y-axis.
TODO: PR a method into ``pyqtgraph`` to make this configurable
"""
'''
if axis in (0, 1):
mask = [False, False]
mask[axis] = self.state['mouseEnabled'][axis]
else:
mask = self.state['mouseEnabled'][:]
chart = self.linked_charts.chart
chart = self.linkedsplits.chart
# don't zoom more then the min points setting
l, lbar, rbar, r = chart.bars_range()
@ -573,7 +616,6 @@ class ChartView(ViewBox):
end_of_l1,
key=lambda p: p.x()
)
# breakpoint()
# focal = pg.Point(last_bar.x() + end_of_l1)
self._resetTarget()
@ -697,22 +739,26 @@ class ChartView(ViewBox):
ev.accept()
self.mode.submit_exec()
def keyReleaseEvent(self, ev: QtCore.QEvent):
# def keyReleaseEvent(self, ev: QtCore.QEvent):
def keyReleaseEvent(self, event: QtCore.QEvent) -> None:
'''This routine is rerouted to an async handler.
'''
pass
async def on_key_release(
self,
text: str,
key: int, # 3-digit
mods: int, # 7-digit
) -> None:
"""
Key release to normally to trigger release of input mode
"""
# TODO: is there a global setting for this?
if ev.isAutoRepeat():
ev.ignore()
return
ev.accept()
# text = ev.text()
key = ev.key()
mods = ev.modifiers()
if key == QtCore.Qt.Key_Shift:
if key == Qt.Key_Shift:
# if self.state['mouseMode'] == ViewBox.RectMode:
self.setMouseMode(ViewBox.PanMode)
@ -722,39 +768,37 @@ class ChartView(ViewBox):
# if self.state['mouseMode'] == ViewBox.RectMode:
# if key == QtCore.Qt.Key_Space:
if mods == QtCore.Qt.ControlModifier or key == QtCore.Qt.Key_Control:
if mods == Qt.ControlModifier or key == QtCore.Qt.Key_Control:
self.mode._exec_mode = 'dark'
if key in {QtCore.Qt.Key_A, QtCore.Qt.Key_F, QtCore.Qt.Key_D}:
if key in {Qt.Key_A, Qt.Key_F, Qt.Key_D}:
# remove "staged" level line under cursor position
self.mode.lines.unstage_line()
self._key_active = False
def keyPressEvent(self, ev: QtCore.QEvent) -> None:
"""
This routine should capture key presses in the current view box.
def keyPressEvent(self, event: QtCore.QEvent) -> None:
'''This routine is rerouted to an async handler.
'''
pass
"""
# TODO: is there a global setting for this?
if ev.isAutoRepeat():
ev.ignore()
return
async def on_key_press(
ev.accept()
text = ev.text()
key = ev.key()
mods = ev.modifiers()
self,
print(f'text: {text}, key: {key}')
text: str,
key: int, # 3-digit
mods: int, # 7-digit
if mods == QtCore.Qt.ShiftModifier:
) -> None:
if mods == Qt.ShiftModifier:
if self.state['mouseMode'] == ViewBox.PanMode:
self.setMouseMode(ViewBox.RectMode)
# ctrl
ctrl = False
if mods == QtCore.Qt.ControlModifier:
if mods == Qt.ControlModifier:
ctrl = True
self.mode._exec_mode = 'live'
@ -767,20 +811,20 @@ class ChartView(ViewBox):
# ctlr-<space>/<l> for "lookup", "search" -> open search tree
if ctrl and key in {
QtCore.Qt.Key_L,
QtCore.Qt.Key_Space,
Qt.Key_L,
Qt.Key_Space,
}:
search = self._chart._lc.chart_space.search
search = self._chart._lc.godwidget.search
search.focus()
# esc
if key == QtCore.Qt.Key_Escape or (ctrl and key == QtCore.Qt.Key_C):
if key == Qt.Key_Escape or (ctrl and key == Qt.Key_C):
# ctrl-c as cancel
# https://forum.qt.io/topic/532/how-to-catch-ctrl-c-on-a-widget/9
self.select_box.clear()
# cancel order or clear graphics
if key == QtCore.Qt.Key_C or key == QtCore.Qt.Key_Delete:
if key == Qt.Key_C or key == Qt.Key_Delete:
# delete any lines under the cursor
mode = self.mode
for line in mode.lines.lines_under_cursor():
@ -789,18 +833,18 @@ class ChartView(ViewBox):
self._key_buffer.append(text)
# View modes
if key == QtCore.Qt.Key_R:
if key == Qt.Key_R:
self.chart.default_view()
# Order modes: stage orders at the current cursor level
elif key == QtCore.Qt.Key_D: # for "damp eet"
elif key == Qt.Key_D: # for "damp eet"
self.mode.set_exec('sell')
elif key == QtCore.Qt.Key_F: # for "fillz eet"
elif key == Qt.Key_F: # for "fillz eet"
self.mode.set_exec('buy')
elif key == QtCore.Qt.Key_A:
elif key == Qt.Key_A:
self.mode.set_exec('alert')
# XXX: Leaving this for light reference purposes, there
@ -808,7 +852,7 @@ class ChartView(ViewBox):
# Key presses are used only when mouse mode is RectMode
# The following events are implemented:
# ctrl-A : zooms out to the default "full" view of the plot
# ctrl-A : zooms out to the default "full" self of the plot
# ctrl-+ : moves forward in the zooming stack (if it exists)
# ctrl-- : moves backward in the zooming stack (if it exists)
@ -819,5 +863,5 @@ class ChartView(ViewBox):
# self.scaleHistory(len(self.axHistory))
else:
# maybe propagate to parent widget
ev.ignore()
# event.ignore()
self._key_active = False

View File

@ -447,7 +447,7 @@ class SearchBar(QtWidgets.QLineEdit):
self.view: CompleterView = view
self.dpi_font = font
self.chart_app = parent_chart
self.godwidget = parent_chart
# size it as we specify
# https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
@ -496,12 +496,12 @@ class SearchWidget(QtGui.QWidget):
def __init__(
self,
chart_space: 'ChartSpace', # type: ignore # noqa
godwidget: 'GodWidget', # type: ignore # noqa
columns: List[str] = ['src', 'symbol'],
parent=None,
) -> None:
super().__init__(parent or chart_space)
super().__init__(parent or godwidget)
# size it as we specify
self.setSizePolicy(
@ -509,7 +509,7 @@ class SearchWidget(QtGui.QWidget):
QtWidgets.QSizePolicy.Fixed,
)
self.chart_app = chart_space
self.godwidget = godwidget
self.vbox = QtGui.QVBoxLayout(self)
self.vbox.setContentsMargins(0, 0, 0, 0)
@ -540,7 +540,7 @@ class SearchWidget(QtGui.QWidget):
)
self.bar = SearchBar(
parent=self,
parent_chart=chart_space,
parent_chart=godwidget,
view=self.view,
)
self.bar_hbox.addWidget(self.bar)
@ -557,7 +557,7 @@ class SearchWidget(QtGui.QWidget):
# fill cache list if nothing existing
self.view.set_section_entries(
'cache',
list(reversed(self.chart_app._chart_cache)),
list(reversed(self.godwidget._chart_cache)),
clear_all=True,
)
@ -611,7 +611,7 @@ class SearchWidget(QtGui.QWidget):
return None
provider, symbol = value
chart = self.chart_app
chart = self.godwidget
log.info(f'Requesting symbol: {symbol}.{provider}')
@ -632,7 +632,7 @@ class SearchWidget(QtGui.QWidget):
# Re-order the symbol cache on the chart to display in
# LIFO order. this is normally only done internally by
# the chart on new symbols being loaded into memory
chart.set_chart_symbol(fqsn, chart.linkedcharts)
chart.set_chart_symbol(fqsn, chart.linkedsplits)
self.view.set_section_entries(
'cache',
@ -650,6 +650,7 @@ _search_enabled: bool = False
async def pack_matches(
view: CompleterView,
has_results: dict[str, set[str]],
matches: dict[(str, str), [str]],
@ -823,7 +824,7 @@ async def fill_results(
async def handle_keyboard_input(
search: SearchWidget,
searchbar: SearchBar,
recv_chan: trio.abc.ReceiveChannel,
) -> None:
@ -831,8 +832,9 @@ async def handle_keyboard_input(
global _search_active, _search_enabled
# startup
chart = search.chart_app
bar = search.bar
bar = searchbar
search = searchbar.parent()
chart = search.godwidget
view = bar.view
view.set_font_size(bar.dpi_font.px_size)
@ -851,7 +853,7 @@ async def handle_keyboard_input(
)
)
async for event, key, mods, txt in recv_chan:
async for event, etype, key, mods, txt in recv_chan:
log.debug(f'key: {key}, mods: {mods}, txt: {txt}')
@ -889,7 +891,7 @@ async def handle_keyboard_input(
# kill the search and focus back on main chart
if chart:
chart.linkedcharts.focus()
chart.linkedsplits.focus()
continue