2020-11-06 17:23:14 +00:00
|
|
|
# piker: trading gear for hackers
|
2021-01-05 18:37:03 +00:00
|
|
|
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
2020-11-06 17:23:14 +00:00
|
|
|
|
|
|
|
# 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/>.
|
|
|
|
|
2020-08-15 02:17:57 +00:00
|
|
|
"""
|
2021-03-18 17:33:10 +00:00
|
|
|
Chart view box primitives
|
|
|
|
|
2020-08-15 02:17:57 +00:00
|
|
|
"""
|
2021-06-15 22:19:59 +00:00
|
|
|
from contextlib import asynccontextmanager
|
2021-06-17 20:52:54 +00:00
|
|
|
import time
|
|
|
|
from typing import Optional, Callable
|
2020-12-30 17:55:02 +00:00
|
|
|
|
2020-08-15 02:17:57 +00:00
|
|
|
import pyqtgraph as pg
|
2021-06-15 23:02:46 +00:00
|
|
|
from PyQt5.QtCore import Qt, QEvent
|
|
|
|
from pyqtgraph import ViewBox, Point, QtCore
|
2020-08-15 02:17:57 +00:00
|
|
|
from pyqtgraph import functions as fn
|
2020-11-08 21:15:34 +00:00
|
|
|
import numpy as np
|
2021-06-15 22:19:59 +00:00
|
|
|
import trio
|
2020-08-15 02:17:57 +00:00
|
|
|
|
|
|
|
from ..log import get_logger
|
2021-06-15 23:02:46 +00:00
|
|
|
from ._style import _min_points_to_show
|
|
|
|
from ._editors import SelectRect
|
2020-08-15 02:17:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
log = get_logger(__name__)
|
|
|
|
|
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
async def handle_viewmode_inputs(
|
|
|
|
|
|
|
|
view: 'ChartView',
|
|
|
|
recv_chan: trio.abc.ReceiveChannel,
|
|
|
|
|
|
|
|
) -> None:
|
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
mode = view.mode
|
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
# track edge triggered keys
|
|
|
|
# (https://en.wikipedia.org/wiki/Interrupt#Triggering_methods)
|
|
|
|
pressed: set[str] = set()
|
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
last = time.time()
|
|
|
|
trigger_mode: str
|
|
|
|
action: str
|
|
|
|
|
|
|
|
# for quick key sequence-combo pattern matching
|
|
|
|
# we have a min_tap period and these should not
|
|
|
|
# ever be auto-repeats since we filter those at the
|
|
|
|
# event filter level prior to the above mem chan.
|
|
|
|
min_tap = 1/6
|
|
|
|
fast_key_seq: list[str] = []
|
|
|
|
fast_taps: dict[str, Callable] = {
|
|
|
|
'cc': mode.cancel_all_orders,
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
async for event, etype, key, mods, text in recv_chan:
|
|
|
|
log.debug(f'key: {key}, mods: {mods}, text: {text}')
|
2021-06-17 20:52:54 +00:00
|
|
|
now = time.time()
|
|
|
|
period = now - last
|
2021-06-15 22:19:59 +00:00
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
# reset mods
|
|
|
|
ctrl: bool = False
|
|
|
|
shift: bool = False
|
|
|
|
|
|
|
|
# press branch
|
2021-06-15 22:19:59 +00:00
|
|
|
if etype in {QEvent.KeyPress}:
|
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
pressed.add(key)
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
if (
|
|
|
|
# clear any old values not part of a "fast" tap sequence:
|
|
|
|
# presumes the period since last tap is longer then our
|
|
|
|
# min_tap period
|
|
|
|
fast_key_seq and period >= min_tap or
|
|
|
|
|
|
|
|
# don't support more then 2 key sequences for now
|
|
|
|
len(fast_key_seq) > 2
|
|
|
|
):
|
|
|
|
fast_key_seq.clear()
|
|
|
|
|
|
|
|
# capture key to fast tap sequence if we either
|
|
|
|
# have no previous keys or we do and the min_tap period is
|
|
|
|
# met
|
|
|
|
if (
|
|
|
|
not fast_key_seq or
|
|
|
|
period <= min_tap and fast_key_seq
|
|
|
|
):
|
|
|
|
fast_key_seq.append(text)
|
|
|
|
log.debug(f'fast keys seqs {fast_key_seq}')
|
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
# mods run through
|
2021-06-16 09:24:04 +00:00
|
|
|
if mods == Qt.ShiftModifier:
|
2021-06-16 12:27:34 +00:00
|
|
|
shift = True
|
2021-06-16 09:24:04 +00:00
|
|
|
|
|
|
|
if mods == Qt.ControlModifier:
|
|
|
|
ctrl = True
|
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
# SEARCH MODE #
|
2021-06-16 09:24:04 +00:00
|
|
|
# ctlr-<space>/<l> for "lookup", "search" -> open search tree
|
2021-06-16 12:27:34 +00:00
|
|
|
if (
|
|
|
|
ctrl and key in {
|
|
|
|
Qt.Key_L,
|
|
|
|
Qt.Key_Space,
|
|
|
|
}
|
|
|
|
):
|
2021-07-22 15:23:41 +00:00
|
|
|
view._chart.linked.godwidget.search.focus()
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
# esc and ctrl-c
|
2021-06-16 09:24:04 +00:00
|
|
|
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()
|
|
|
|
|
|
|
|
# cancel order or clear graphics
|
|
|
|
if key == Qt.Key_C or key == Qt.Key_Delete:
|
2021-06-16 12:27:34 +00:00
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
mode.cancel_orders_under_cursor()
|
2021-06-16 09:24:04 +00:00
|
|
|
|
|
|
|
# View modes
|
|
|
|
if key == Qt.Key_R:
|
2021-06-16 12:27:34 +00:00
|
|
|
|
|
|
|
# edge triggered default view activation
|
2021-06-16 09:24:04 +00:00
|
|
|
view.chart.default_view()
|
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
if len(fast_key_seq) > 1:
|
|
|
|
# begin matches against sequences
|
|
|
|
func: Callable = fast_taps.get(''.join(fast_key_seq))
|
|
|
|
if func:
|
|
|
|
func()
|
|
|
|
fast_key_seq.clear()
|
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
# release branch
|
|
|
|
elif etype in {QEvent.KeyRelease}:
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
if key in pressed:
|
|
|
|
pressed.remove(key)
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-06-22 11:17:49 +00:00
|
|
|
# QUERY MODE #
|
|
|
|
if {Qt.Key_Q}.intersection(pressed):
|
|
|
|
|
|
|
|
view.linkedsplits.cursor.in_query_mode = True
|
|
|
|
|
|
|
|
else:
|
|
|
|
view.linkedsplits.cursor.in_query_mode = False
|
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
# SELECTION MODE #
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
if shift:
|
|
|
|
if view.state['mouseMode'] == ViewBox.PanMode:
|
|
|
|
view.setMouseMode(ViewBox.RectMode)
|
|
|
|
else:
|
|
|
|
view.setMouseMode(ViewBox.PanMode)
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-07-26 15:33:14 +00:00
|
|
|
# Toggle position config pane
|
|
|
|
if (
|
|
|
|
ctrl and key in {
|
|
|
|
Qt.Key_P,
|
|
|
|
}
|
|
|
|
):
|
|
|
|
pp_conf = mode.pp_config
|
|
|
|
if pp_conf.isHidden():
|
|
|
|
pp_conf.show()
|
|
|
|
else:
|
|
|
|
pp_conf.hide()
|
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
# ORDER MODE #
|
|
|
|
# live vs. dark trigger + an action {buy, sell, alert}
|
|
|
|
order_keys_pressed = {
|
|
|
|
Qt.Key_A,
|
|
|
|
Qt.Key_F,
|
|
|
|
Qt.Key_D
|
|
|
|
}.intersection(pressed)
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
if order_keys_pressed:
|
2021-07-22 00:00:57 +00:00
|
|
|
|
|
|
|
# show the pp label
|
|
|
|
mode.pp.show()
|
2021-07-26 15:33:14 +00:00
|
|
|
|
|
|
|
# TODO: show pp config mini-params in status bar widget
|
|
|
|
# mode.pp_config.show()
|
2021-07-22 00:00:57 +00:00
|
|
|
|
2021-06-17 20:52:54 +00:00
|
|
|
if (
|
|
|
|
# 's' for "submit" to activate "live" order
|
|
|
|
Qt.Key_S in pressed or
|
|
|
|
ctrl
|
|
|
|
):
|
|
|
|
trigger_mode: str = 'live'
|
|
|
|
|
|
|
|
else:
|
|
|
|
trigger_mode: str = 'dark'
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
# order mode trigger "actions"
|
|
|
|
if Qt.Key_D in pressed: # for "damp eet"
|
2021-06-17 20:52:54 +00:00
|
|
|
action = 'sell'
|
2021-06-16 09:24:04 +00:00
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
elif Qt.Key_F in pressed: # for "fillz eet"
|
2021-06-17 20:52:54 +00:00
|
|
|
action = 'buy'
|
2021-06-16 12:27:34 +00:00
|
|
|
|
|
|
|
elif Qt.Key_A in pressed:
|
2021-06-17 20:52:54 +00:00
|
|
|
action = 'alert'
|
|
|
|
trigger_mode = 'live'
|
|
|
|
|
|
|
|
view.order_mode = True
|
|
|
|
|
|
|
|
# XXX: order matters here for line style!
|
|
|
|
view.mode._exec_mode = trigger_mode
|
|
|
|
view.mode.set_exec(action)
|
|
|
|
|
|
|
|
prefix = trigger_mode + '-' if action != 'alert' else ''
|
2021-07-24 20:07:04 +00:00
|
|
|
view._chart.window().set_mode_name(f'{prefix}{action}')
|
2021-06-16 12:27:34 +00:00
|
|
|
|
|
|
|
else: # none active
|
2021-07-22 00:00:57 +00:00
|
|
|
|
|
|
|
# hide pp label
|
2021-07-22 15:23:41 +00:00
|
|
|
mode.pp.hide_info()
|
2021-07-26 15:33:14 +00:00
|
|
|
# mode.pp_config.hide()
|
2021-07-22 00:00:57 +00:00
|
|
|
|
2021-06-16 12:27:34 +00:00
|
|
|
# if none are pressed, remove "staged" level
|
|
|
|
# line under cursor position
|
|
|
|
view.mode.lines.unstage_line()
|
2021-06-17 20:52:54 +00:00
|
|
|
|
|
|
|
if view.hasFocus():
|
|
|
|
# update mode label
|
2021-07-24 20:07:04 +00:00
|
|
|
view._chart.window().set_mode_name('view')
|
2021-06-17 20:52:54 +00:00
|
|
|
|
|
|
|
view.order_mode = False
|
|
|
|
|
|
|
|
last = time.time()
|
2021-06-15 22:19:59 +00:00
|
|
|
|
|
|
|
|
2020-11-08 21:15:34 +00:00
|
|
|
class ChartView(ViewBox):
|
2021-06-15 22:19:59 +00:00
|
|
|
'''
|
|
|
|
Price chart view box with interaction behaviors you'd expect from
|
2020-08-15 02:17:57 +00:00
|
|
|
any interactive platform:
|
|
|
|
|
|
|
|
- zoom on mouse scroll that auto fits y-axis
|
2021-01-05 13:02:14 +00:00
|
|
|
- vertical scrolling on y-axis
|
|
|
|
- zoom on x to most recent in view datum
|
|
|
|
- zoom on right-click-n-drag to cursor position
|
2021-01-07 17:03:18 +00:00
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
'''
|
2021-07-24 20:07:04 +00:00
|
|
|
mode_name: str = 'view'
|
2021-05-30 12:45:55 +00:00
|
|
|
|
2020-08-15 02:17:57 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2021-07-22 00:00:57 +00:00
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
name: str,
|
2021-07-22 00:00:57 +00:00
|
|
|
|
2020-11-08 21:15:34 +00:00
|
|
|
parent: pg.PlotItem = None,
|
2020-08-15 02:17:57 +00:00
|
|
|
**kwargs,
|
2021-06-15 22:19:59 +00:00
|
|
|
|
2020-08-15 02:17:57 +00:00
|
|
|
):
|
|
|
|
super().__init__(parent=parent, **kwargs)
|
2021-06-15 22:19:59 +00:00
|
|
|
|
2020-08-15 02:17:57 +00:00
|
|
|
# disable vertical scrolling
|
|
|
|
self.setMouseEnabled(x=True, y=False)
|
2021-06-15 22:19:59 +00:00
|
|
|
|
|
|
|
self.linkedsplits = None
|
|
|
|
self._chart: 'ChartPlotWidget' = None # noqa
|
|
|
|
|
|
|
|
# add our selection box annotator
|
2020-11-08 21:15:34 +00:00
|
|
|
self.select_box = SelectRect(self)
|
|
|
|
self.addItem(self.select_box, ignoreBounds=True)
|
|
|
|
|
2021-01-07 17:03:18 +00:00
|
|
|
self.mode = None
|
2021-06-17 20:52:54 +00:00
|
|
|
self.order_mode: bool = False
|
2021-01-07 17:03:18 +00:00
|
|
|
|
2021-04-12 22:04:26 +00:00
|
|
|
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
@asynccontextmanager
|
|
|
|
async def open_async_input_handler(
|
|
|
|
self,
|
2021-07-22 00:00:57 +00:00
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
) -> 'ChartView':
|
|
|
|
from . import _event
|
|
|
|
|
2021-07-25 19:42:48 +00:00
|
|
|
async with _event.open_handlers(
|
|
|
|
[self],
|
2021-06-15 22:19:59 +00:00
|
|
|
event_types={QEvent.KeyPress, QEvent.KeyRelease},
|
|
|
|
async_handler=handle_viewmode_inputs,
|
|
|
|
):
|
|
|
|
yield self
|
|
|
|
|
2020-11-08 21:15:34 +00:00
|
|
|
@property
|
2021-01-01 18:05:50 +00:00
|
|
|
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
|
2020-11-08 21:15:34 +00:00
|
|
|
return self._chart
|
|
|
|
|
|
|
|
@chart.setter
|
2021-01-01 18:05:50 +00:00
|
|
|
def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa
|
2020-11-08 21:15:34 +00:00
|
|
|
self._chart = chart
|
|
|
|
self.select_box.chart = chart
|
2020-08-15 02:17:57 +00:00
|
|
|
|
|
|
|
def wheelEvent(self, ev, axis=None):
|
2021-06-15 22:19:59 +00:00
|
|
|
'''Override "center-point" location for scrolling.
|
2020-08-15 02:17:57 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
'''
|
2020-08-15 02:17:57 +00:00
|
|
|
if axis in (0, 1):
|
|
|
|
mask = [False, False]
|
|
|
|
mask[axis] = self.state['mouseEnabled'][axis]
|
|
|
|
else:
|
|
|
|
mask = self.state['mouseEnabled'][:]
|
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
chart = self.linkedsplits.chart
|
2021-03-17 12:25:58 +00:00
|
|
|
|
2020-08-15 02:17:57 +00:00
|
|
|
# don't zoom more then the min points setting
|
2021-03-17 12:25:58 +00:00
|
|
|
l, lbar, rbar, r = chart.bars_range()
|
2020-08-15 02:17:57 +00:00
|
|
|
vl = r - l
|
|
|
|
|
|
|
|
if ev.delta() > 0 and vl <= _min_points_to_show:
|
2020-10-20 01:32:50 +00:00
|
|
|
log.debug("Max zoom bruh...")
|
2020-08-15 02:17:57 +00:00
|
|
|
return
|
2020-10-25 14:49:31 +00:00
|
|
|
|
2021-06-22 11:17:49 +00:00
|
|
|
if ev.delta() < 0 and vl >= len(chart._arrays['ohlc']) + 666:
|
2020-10-20 01:32:50 +00:00
|
|
|
log.debug("Min zoom bruh...")
|
2020-08-15 02:17:57 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
# actual scaling factor
|
|
|
|
s = 1.015 ** (ev.delta() * -1 / 20) # self.state['wheelScaleFactor'])
|
|
|
|
s = [(None if m is False else s) for m in mask]
|
|
|
|
|
|
|
|
# center = pg.Point(
|
|
|
|
# fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
|
|
|
|
# )
|
|
|
|
|
|
|
|
# XXX: scroll "around" the right most element in the view
|
2020-10-20 01:32:50 +00:00
|
|
|
# which stays "pinned" in place.
|
|
|
|
|
|
|
|
# furthest_right_coord = self.boundingRect().topRight()
|
|
|
|
|
|
|
|
# yaxis = pg.Point(
|
|
|
|
# fn.invertQTransform(
|
|
|
|
# self.childGroup.transform()
|
|
|
|
# ).map(furthest_right_coord)
|
|
|
|
# )
|
|
|
|
|
2020-12-10 20:44:20 +00:00
|
|
|
# This seems like the most "intuitive option, a hybrid of
|
2020-10-20 01:32:50 +00:00
|
|
|
# tws and tv styles
|
2021-03-17 12:25:58 +00:00
|
|
|
last_bar = pg.Point(int(rbar)) + 1
|
|
|
|
|
|
|
|
ryaxis = chart.getAxis('right')
|
|
|
|
r_axis_x = ryaxis.pos().x()
|
|
|
|
|
|
|
|
end_of_l1 = pg.Point(
|
|
|
|
round(
|
|
|
|
chart._vb.mapToView(
|
|
|
|
pg.Point(r_axis_x - chart._max_l1_line_len)
|
|
|
|
# QPointF(chart._max_l1_line_len, 0)
|
|
|
|
).x()
|
|
|
|
)
|
|
|
|
) # .x()
|
|
|
|
|
|
|
|
# self.state['viewRange'][0][1] = end_of_l1
|
|
|
|
|
|
|
|
# focal = pg.Point((last_bar.x() + end_of_l1)/2)
|
|
|
|
|
|
|
|
focal = min(
|
|
|
|
last_bar,
|
|
|
|
end_of_l1,
|
|
|
|
key=lambda p: p.x()
|
|
|
|
)
|
|
|
|
# focal = pg.Point(last_bar.x() + end_of_l1)
|
2020-08-15 02:17:57 +00:00
|
|
|
|
|
|
|
self._resetTarget()
|
2021-03-17 12:25:58 +00:00
|
|
|
self.scaleBy(s, focal)
|
2020-08-15 02:17:57 +00:00
|
|
|
ev.accept()
|
|
|
|
self.sigRangeChangedManually.emit(mask)
|
2020-11-08 21:15:34 +00:00
|
|
|
|
2020-12-30 17:55:02 +00:00
|
|
|
def mouseDragEvent(
|
|
|
|
self,
|
|
|
|
ev,
|
|
|
|
axis: Optional[int] = None,
|
|
|
|
) -> None:
|
2020-11-08 21:15:34 +00:00
|
|
|
# if axis is specified, event will only affect that axis.
|
|
|
|
ev.accept() # we accept all buttons
|
2021-01-01 22:49:23 +00:00
|
|
|
button = ev.button()
|
2020-11-08 21:15:34 +00:00
|
|
|
|
|
|
|
pos = ev.pos()
|
|
|
|
lastPos = ev.lastPos()
|
|
|
|
dif = pos - lastPos
|
|
|
|
dif = dif * -1
|
|
|
|
|
|
|
|
# Ignore axes if mouse is disabled
|
|
|
|
mouseEnabled = np.array(self.state['mouseEnabled'], dtype=np.float)
|
|
|
|
mask = mouseEnabled.copy()
|
|
|
|
if axis is not None:
|
|
|
|
mask[1-axis] = 0.0
|
|
|
|
|
|
|
|
# Scale or translate based on mouse button
|
2021-01-01 22:49:23 +00:00
|
|
|
if button & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton):
|
|
|
|
|
2021-01-05 13:02:14 +00:00
|
|
|
# zoom y-axis ONLY when click-n-drag on it
|
2020-12-30 17:55:02 +00:00
|
|
|
if axis == 1:
|
|
|
|
# set a static y range special value on chart widget to
|
|
|
|
# prevent sizing to data in view.
|
2021-01-01 18:05:50 +00:00
|
|
|
self.chart._static_yrange = 'axis'
|
2020-12-30 17:55:02 +00:00
|
|
|
|
|
|
|
scale_y = 1.3 ** (dif.y() * -1 / 20)
|
|
|
|
self.setLimits(yMin=None, yMax=None)
|
|
|
|
|
|
|
|
# print(scale_y)
|
|
|
|
self.scaleBy((0, scale_y))
|
|
|
|
|
2020-11-08 21:15:34 +00:00
|
|
|
if self.state['mouseMode'] == ViewBox.RectMode:
|
|
|
|
|
|
|
|
down_pos = ev.buttonDownPos()
|
|
|
|
|
|
|
|
# This is the final position in the drag
|
|
|
|
if ev.isFinish():
|
|
|
|
|
|
|
|
self.select_box.mouse_drag_released(down_pos, pos)
|
|
|
|
|
|
|
|
# ax = QtCore.QRectF(down_pos, pos)
|
|
|
|
# ax = self.childGroup.mapRectFromParent(ax)
|
|
|
|
# print(ax)
|
|
|
|
|
|
|
|
# this is the zoom transform cmd
|
|
|
|
# self.showAxRect(ax)
|
|
|
|
|
|
|
|
# self.axHistoryPointer += 1
|
|
|
|
# self.axHistory = self.axHistory[
|
|
|
|
# :self.axHistoryPointer] + [ax]
|
|
|
|
else:
|
|
|
|
self.select_box.set_pos(down_pos, pos)
|
|
|
|
|
|
|
|
# update shape of scale box
|
|
|
|
# self.updateScaleBox(ev.buttonDownPos(), ev.pos())
|
|
|
|
else:
|
2021-01-01 22:49:23 +00:00
|
|
|
# default bevavior: click to pan view
|
|
|
|
|
2020-11-08 21:15:34 +00:00
|
|
|
tr = self.childGroup.transform()
|
|
|
|
tr = fn.invertQTransform(tr)
|
|
|
|
tr = tr.map(dif*mask) - tr.map(Point(0, 0))
|
|
|
|
|
|
|
|
x = tr.x() if mask[0] == 1 else None
|
|
|
|
y = tr.y() if mask[1] == 1 else None
|
|
|
|
|
|
|
|
self._resetTarget()
|
2021-01-01 22:49:23 +00:00
|
|
|
|
2020-11-08 21:15:34 +00:00
|
|
|
if x is not None or y is not None:
|
|
|
|
self.translateBy(x=x, y=y)
|
2021-01-01 22:49:23 +00:00
|
|
|
|
2020-11-08 21:15:34 +00:00
|
|
|
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
|
|
|
|
2021-01-01 22:49:23 +00:00
|
|
|
elif button & QtCore.Qt.RightButton:
|
|
|
|
|
|
|
|
# right click zoom to center behaviour
|
2020-12-30 17:55:02 +00:00
|
|
|
|
2020-11-08 21:15:34 +00:00
|
|
|
if self.state['aspectLocked'] is not False:
|
|
|
|
mask[0] = 0
|
|
|
|
|
|
|
|
dif = ev.screenPos() - ev.lastScreenPos()
|
|
|
|
dif = np.array([dif.x(), dif.y()])
|
|
|
|
dif[0] *= -1
|
|
|
|
s = ((mask * 0.02) + 1) ** dif
|
|
|
|
|
|
|
|
tr = self.childGroup.transform()
|
|
|
|
tr = fn.invertQTransform(tr)
|
|
|
|
|
|
|
|
x = s[0] if mouseEnabled[0] == 1 else None
|
|
|
|
y = s[1] if mouseEnabled[1] == 1 else None
|
|
|
|
|
|
|
|
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
|
|
|
|
self._resetTarget()
|
|
|
|
self.scaleBy(x=x, y=y, center=center)
|
|
|
|
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
|
|
|
|
2021-01-01 22:49:23 +00:00
|
|
|
def mouseClickEvent(self, ev):
|
|
|
|
"""Full-click callback.
|
|
|
|
|
|
|
|
"""
|
|
|
|
button = ev.button()
|
2021-01-05 18:37:03 +00:00
|
|
|
# pos = ev.pos()
|
2021-01-01 22:49:23 +00:00
|
|
|
|
2021-01-03 16:12:51 +00:00
|
|
|
if button == QtCore.Qt.RightButton and self.menuEnabled():
|
2021-01-01 22:49:23 +00:00
|
|
|
ev.accept()
|
|
|
|
self.raiseContextMenu(ev)
|
|
|
|
|
|
|
|
elif button == QtCore.Qt.LeftButton:
|
2021-01-12 02:23:39 +00:00
|
|
|
# when in order mode, submit execution
|
2021-06-17 20:52:54 +00:00
|
|
|
if self.order_mode:
|
2021-01-20 01:48:58 +00:00
|
|
|
ev.accept()
|
|
|
|
self.mode.submit_exec()
|
2021-01-07 17:03:18 +00:00
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
def keyReleaseEvent(self, event: QtCore.QEvent) -> None:
|
|
|
|
'''This routine is rerouted to an async handler.
|
|
|
|
'''
|
|
|
|
pass
|
|
|
|
|
|
|
|
def keyPressEvent(self, event: QtCore.QEvent) -> None:
|
|
|
|
'''This routine is rerouted to an async handler.
|
|
|
|
'''
|
|
|
|
pass
|