From 94ebe1e87ea120a20943e7721ac9617801d0fcba Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 2 Aug 2023 20:41:56 -0400 Subject: [PATCH] Add some new hotkey maps for chart zoom and pane hiding --- piker/ui/_chart.py | 4 +- piker/ui/_forms.py | 1 - piker/ui/_fsp.py | 1 + piker/ui/_interaction.py | 157 ++++++++++++++++++++++++++++++++------- 4 files changed, 134 insertions(+), 29 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 28e258f3..e00ad70b 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -406,6 +406,7 @@ class ChartnPane(QFrame): ) self._sidepane = sidepane + @property def sidepane(self) -> FieldsForm | SearchWidget: return self._sidepane @@ -495,7 +496,7 @@ class LinkedSplits(QWidget): Set the proportion of space allocated for linked subcharts. ''' - ln = len(self.subplots) or 1 + ln: int = len(self.subplots) or 1 # proportion allocated to consumer subcharts if not prop: @@ -925,6 +926,7 @@ class ChartPlotWidget(pg.PlotWidget): self.useOpenGL(use_open_gl) self.name = name self.data_key = data_key or name + self.qframe: ChartnPane | None = None # scene-local placeholder for book graphics # sizing to avoid overlap with data contents diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index a86cf903..d3f8da73 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -21,7 +21,6 @@ Text entry "forms" widgets (mostly for configuration and UI user input). from __future__ import annotations from contextlib import asynccontextmanager from functools import partial -from math import floor from typing import ( Any, Callable, diff --git a/piker/ui/_fsp.py b/piker/ui/_fsp.py index a4deb034..bcbd95d4 100644 --- a/piker/ui/_fsp.py +++ b/piker/ui/_fsp.py @@ -283,6 +283,7 @@ async def run_fsp_ui( name, array_key=array_key, ) + assert chart.qframe chart.linked.focus() diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index cf468735..e1df4066 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -32,8 +32,18 @@ from typing import ( import pyqtgraph as pg # from pyqtgraph.GraphicsScene import mouseEvents from PyQt5.QtWidgets import QGraphicsSceneMouseEvent as gs_mouse -from PyQt5.QtCore import Qt, QEvent -from pyqtgraph import ViewBox, Point, QtCore +from PyQt5.QtGui import ( + QWheelEvent, +) +from PyQt5.QtCore import ( + Qt, + QEvent, +) +from pyqtgraph import ( + ViewBox, + Point, + QtCore, +) from pyqtgraph import functions as fn import numpy as np import trio @@ -50,8 +60,16 @@ from ._editors import SelectRect from . import _event if TYPE_CHECKING: - from ._chart import ChartPlotWidget + # from ._search import ( + # SearchWidget, + # ) + from ._chart import ( + ChartnPane, + ChartPlotWidget, + GodWidget, + ) from ._dataviz import Viz + from .order_mode import OrderMode log = get_logger(__name__) @@ -83,7 +101,8 @@ async def handle_viewmode_kb_inputs( ) -> None: - order_mode = view.order_mode + order_mode: OrderMode = view.order_mode + godw: GodWidget = order_mode.godw # noqa # track edge triggered keys # (https://en.wikipedia.org/wiki/Interrupt#Triggering_methods) @@ -147,14 +166,14 @@ async def handle_viewmode_kb_inputs( if mods == Qt.ControlModifier: ctrl = True - # UI REPL-shell + # UI REPL-shell, with ctrl-p (for "pause") if ( - ctrl and key in { - Qt.Key_U, + ctrl + and key in { + Qt.Key_P, } ): import tractor - god = order_mode.godw # noqa feed = order_mode.feed # noqa chart = order_mode.chart # noqa viz = chart.main_viz # noqa @@ -167,9 +186,10 @@ async def handle_viewmode_kb_inputs( # SEARCH MODE # # ctlr-/ for "lookup", "search" -> open search tree if ( - ctrl and key in { + ctrl + and key in { Qt.Key_L, - Qt.Key_Space, + # Qt.Key_Space, } ): godw = view._chart.linked.godwidget @@ -177,19 +197,53 @@ async def handle_viewmode_kb_inputs( godw.search.focus() # esc and ctrl-c - if key == Qt.Key_Escape or (ctrl and key == 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 view.select_box.clear() view.linked.focus() # cancel order or clear graphics - if key == Qt.Key_C or key == Qt.Key_Delete: + if ( + key == Qt.Key_C + or key == Qt.Key_Delete + ): order_mode.cancel_orders_under_cursor() # View modes - if key == Qt.Key_R: + if ( + ctrl + and ( + key == Qt.Key_Equal + or key == Qt.Key_I + ) + ): + view.wheelEvent( + ev=None, + axis=None, + delta=view.def_delta, + ) + elif ( + ctrl + and ( + key == Qt.Key_Minus + or key == Qt.Key_O + ) + ): + view.wheelEvent( + ev=None, + axis=None, + delta=-view.def_delta, + ) + + elif key == Qt.Key_R: # NOTE: seems that if we don't yield a Qt render # cycle then the m4 downsampled curves will show here @@ -235,15 +289,47 @@ async def handle_viewmode_kb_inputs( # Toggle position config pane if ( - ctrl and key in { - Qt.Key_P, + ctrl + and key in { + Qt.Key_Space, } ): - pp_pane = order_mode.current_pp.pane - if pp_pane.isHidden(): - pp_pane.show() + # searchw: SearchWidget = godw.search + # pp_pane = order_mode.current_pp.pane + qframes: list[ChartnPane] = [] + + for linked in ( + godw.rt_linked, + godw.hist_linked, + ): + for chartw in ( + [linked.chart] + + + list(linked.subplots.values()) + ): + qframes.append( + chartw.qframe + ) + + # NOTE: place priority on FIRST hiding all + # panes before showing them. + # TODO: make this more "fancy"? + # - maybe look at majority of hidden states and then + # flip based on that? + # - move these loops into the chart APIs? + # - store the UX-state for a given feed/symbol and + # apply when opening a new one (eg. if panes were + # hidden then also hide them on newly loaded mkt + # feeds). + if not any( + qf.sidepane.isHidden() for qf in qframes + ): + for qf in qframes: + qf.sidepane.hide() + else: - pp_pane.hide() + for qf in qframes: + qf.sidepane.show() # ORDER MODE # ---------- @@ -378,6 +464,8 @@ class ChartView(ViewBox): ''' mode_name: str = 'view' + def_delta: float = 616 * 6 + def_scale_factor: float = 1.016 ** (def_delta * -1 / 20) def __init__( self, @@ -502,8 +590,9 @@ class ChartView(ViewBox): def wheelEvent( self, - ev, - axis=None, + ev: QWheelEvent | None = None, + axis: int | None = None, + delta: float | None = None, ): ''' Override "center-point" location for scrolling. @@ -514,6 +603,12 @@ class ChartView(ViewBox): TODO: PR a method into ``pyqtgraph`` to make this configurable ''' + # NOTE: certain operations are only avail when this handler is + # actually called on events. + if ev is None: + assert delta + assert axis is None + linked = self.linked if ( not linked @@ -524,7 +619,7 @@ class ChartView(ViewBox): mask = [False, False] mask[axis] = self.state['mouseEnabled'][axis] else: - mask = self.state['mouseEnabled'][:] + mask: list[bool] = self.state['mouseEnabled'][:] chart = self.linked.chart @@ -545,8 +640,15 @@ class ChartView(ViewBox): # return # actual scaling factor - s = 1.016 ** (ev.delta() * -1 / 20) # self.state['wheelScaleFactor']) - s = [(None if m is False else s) for m in mask] + delta: float = ev.delta() if ev else delta + scale_factor: float = 1.016 ** (delta * -1 / 20) + + # NOTE: if elem is False -> None meaning "do not scale that + # axis". + scales: list[float | bool] = [ + (None if m is False else scale_factor) + for m in mask + ] if ( # zoom happened on axis @@ -569,7 +671,7 @@ class ChartView(ViewBox): ).map(ev.pos()) ) # scale_y = 1.3 ** (center.y() * -1 / 20) - self.scaleBy(s, center) + self.scaleBy(scales, center) # zoom in view-box area else: @@ -584,7 +686,7 @@ class ChartView(ViewBox): # NOTE: scroll "around" the right most datum-element in view # gives the feeling of staying "pinned" in place. - self.scaleBy(s, focal) + self.scaleBy(scales, focal) # XXX: the order of the next 2 lines i'm pretty sure # matters, we want the resize to trigger before the graphics @@ -604,7 +706,8 @@ class ChartView(ViewBox): self.interact_graphics_cycle() self.interact_graphics_cycle() - ev.accept() + if ev: + ev.accept() def mouseDragEvent( self,