Add user defined anchor support to label; reorg mod

fsp_feeds
Tyler Goodlet 2021-07-16 07:45:42 -04:00
parent 86cb8421d9
commit 62517c1662
2 changed files with 195 additions and 169 deletions

View File

@ -32,7 +32,6 @@ import trio
from ..log import get_logger from ..log import get_logger
from ._style import _min_points_to_show from ._style import _min_points_to_show
from ._editors import SelectRect from ._editors import SelectRect
from ._window import main_window
log = get_logger(__name__) log = get_logger(__name__)

View File

@ -19,7 +19,7 @@ Non-shitty labels that don't re-invent the wheel.
""" """
from inspect import isfunction from inspect import isfunction
from typing import Callable from typing import Callable, Optional
import pyqtgraph as pg import pyqtgraph as pg
from PyQt5 import QtGui, QtWidgets from PyQt5 import QtGui, QtWidgets
@ -31,6 +31,200 @@ from ._style import (
) )
class Label:
"""
A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
After hacking for many days on multiple "label" systems inside
``pyqtgraph`` yet again we're left writing our own since it seems
all of those are over complicated, ad-hoc, transform-mangling,
messes which can't accomplish the simplest things via their inputs
(such as pinning to the left hand side of a view box).
Here we do the simple thing where the label uses callables to figure
out the (x, y) coordinate "pin point": nice and simple.
This type is another effort (see our graphics) to start making
small, re-usable label components that can actually be used to build
production grade UIs...
"""
def __init__(
self,
view: pg.ViewBox,
fmt_str: str,
color: str = 'bracket',
x_offset: float = 0,
font_size: str = 'small',
opacity: float = 0.666,
fields: dict = {},
update_on_range_change: bool = True,
) -> None:
vb = self.vb = view
self._fmt_str = fmt_str
self._view_xy = QPointF(0, 0)
self.scene_anchor: Optional[Callable[..., QPointF]] = None
self._x_offset = x_offset
txt = self.txt = QtWidgets.QGraphicsTextItem()
vb.scene().addItem(txt)
# configure font size based on DPI
dpi_font = DpiAwareFont(
font_size=font_size,
)
dpi_font.configure_to_dpi()
txt.setFont(dpi_font.font)
txt.setOpacity(opacity)
# register viewbox callbacks
if update_on_range_change:
vb.sigRangeChanged.connect(self.on_sigrange_change)
self._hcolor: str = ''
self.color = color
self.fields = fields
self.orient_v = 'bottom'
self._anchor_func = self.txt.pos().x
# not sure if this makes a diff
self.txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
# TODO: edit and selection support
# https://doc.qt.io/qt-5/qt.html#TextInteractionFlag-enum
# self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
@property
def color(self):
return self._hcolor
@color.setter
def color(self, color: str) -> None:
self.txt.setDefaultTextColor(pg.mkColor(hcolor(color)))
self._hcolor = color
def update(self) -> None:
'''Update this label either by invoking its
user defined anchoring function, or by positioning
to the last recorded data view coordinates.
'''
# move label in scene coords to desired position
anchor = self.scene_anchor
if anchor:
self.txt.setPos(anchor())
else:
# position based on last computed view coordinate
self.set_view_pos(self._view_xy.y())
def on_sigrange_change(self, vr, r) -> None:
return self.update()
@property
def w(self) -> float:
return self.txt.boundingRect().width()
@property
def h(self) -> float:
return self.txt.boundingRect().height()
def vbr(self) -> QRectF:
return self.vb.boundingRect()
def set_x_anchor_func(
self,
func: Callable,
) -> None:
assert isinstance(func(), float)
self._anchor_func = func
def set_view_pos(
self,
y: float,
x: Optional[float] = None,
) -> None:
if x is None:
scene_x = self._anchor_func() or self.txt.pos().x()
x = self.vb.mapToView(QPointF(scene_x, scene_x)).x()
# get new (inside the) view coordinates / position
self._view_xy = QPointF(x, y)
# map back to the outer UI-land "scene" coordinates
s_xy = self.vb.mapFromView(self._view_xy)
if self.orient_v == 'top':
s_xy = QPointF(s_xy.x(), s_xy.y() - self.h)
# move label in scene coords to desired position
self.txt.setPos(s_xy)
assert s_xy == self.txt.pos()
# def orient_on(self, h: str, v: str) -> None:
# pass
@property
def fmt_str(self) -> str:
return self._fmt_str
@fmt_str.setter
def fmt_str(self, fmt_str: str) -> None:
self._fmt_str = fmt_str
def format(
self,
**fields: dict
) -> str:
out = {}
# this is hacky support for single depth
# calcs of field data from field data
# ex. to calculate a $value = price * size
for k, v in fields.items():
if isfunction(v):
out[k] = v(fields)
else:
out[k] = v
text = self._fmt_str.format(**out)
# for large numbers with a thousands place
text = text.replace(',', ' ')
self.txt.setPlainText(text)
def render(self) -> None:
self.format(**self.fields)
def show(self) -> None:
self.txt.show()
def hide(self) -> None:
self.txt.hide()
def delete(self) -> None:
self.vb.scene().removeItem(self.txt)
def vbr_left(label) -> Callable[..., float]: def vbr_left(label) -> Callable[..., float]:
"""Return a closure which gives the scene x-coordinate for the """Return a closure which gives the scene x-coordinate for the
leftmost point of the containing view box. leftmost point of the containing view box.
@ -85,170 +279,3 @@ def right_axis(
return ryaxis.pos().x() # + axis_offset - 2 return ryaxis.pos().x() # + axis_offset - 2
return on_axis return on_axis
class Label:
"""
A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
After hacking for many days on multiple "label" systems inside
``pyqtgraph`` yet again we're left writing our own since it seems
all of those are over complicated, ad-hoc, transform-mangling,
messes which can't accomplish the simplest things via their inputs
(such as pinning to the left hand side of a view box).
Here we do the simple thing where the label uses callables to figure
out the (x, y) coordinate "pin point": nice and simple.
This type is another effort (see our graphics) to start making
small, re-usable label components that can actually be used to build
production grade UIs...
"""
def __init__(
self,
view: pg.ViewBox,
fmt_str: str,
color: str = 'bracket',
x_offset: float = 0,
font_size: str = 'small',
opacity: float = 0.666,
fields: dict = {}
) -> None:
vb = self.vb = view
self._fmt_str = fmt_str
self._view_xy = QPointF(0, 0)
self._x_offset = x_offset
txt = self.txt = QtWidgets.QGraphicsTextItem()
vb.scene().addItem(txt)
# configure font size based on DPI
dpi_font = DpiAwareFont(
font_size=font_size,
)
dpi_font.configure_to_dpi()
txt.setFont(dpi_font.font)
txt.setOpacity(opacity)
# register viewbox callbacks
vb.sigRangeChanged.connect(self.on_sigrange_change)
self._hcolor: str = ''
self.color = color
self.fields = fields
self.orient_v = 'bottom'
self._anchor_func = self.txt.pos().x
# not sure if this makes a diff
self.txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
# TODO: edit and selection support
# https://doc.qt.io/qt-5/qt.html#TextInteractionFlag-enum
# self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
@property
def color(self):
return self._hcolor
@color.setter
def color(self, color: str) -> None:
self.txt.setDefaultTextColor(pg.mkColor(hcolor(color)))
self._hcolor = color
def on_sigrange_change(self, vr, r) -> None:
self.set_view_y(self._view_xy.y())
@property
def w(self) -> float:
return self.txt.boundingRect().width()
@property
def h(self) -> float:
return self.txt.boundingRect().height()
def vbr(self) -> QRectF:
return self.vb.boundingRect()
def set_x_anchor_func(
self,
func: Callable,
) -> None:
assert isinstance(func(), float)
self._anchor_func = func
def set_view_y(
self,
y: float,
) -> None:
scene_x = self._anchor_func() or self.txt.pos().x()
# get new (inside the) view coordinates / position
self._view_xy = QPointF(
self.vb.mapToView(QPointF(scene_x, scene_x)).x(),
y,
)
# map back to the outer UI-land "scene" coordinates
s_xy = self.vb.mapFromView(self._view_xy)
if self.orient_v == 'top':
s_xy = QPointF(s_xy.x(), s_xy.y() - self.h)
# move label in scene coords to desired position
self.txt.setPos(s_xy)
assert s_xy == self.txt.pos()
def orient_on(self, h: str, v: str) -> None:
pass
@property
def fmt_str(self) -> str:
return self._fmt_str
@fmt_str.setter
def fmt_str(self, fmt_str: str) -> None:
self._fmt_str = fmt_str
def format(self, **fields: dict) -> str:
out = {}
# this is hacky support for single depth
# calcs of field data from field data
# ex. to calculate a $value = price * size
for k, v in fields.items():
if isfunction(v):
out[k] = v(fields)
else:
out[k] = v
text = self._fmt_str.format(**out)
# for large numbers with a thousands place
text = text.replace(',', ' ')
self.txt.setPlainText(text)
def render(self) -> None:
self.format(**self.fields)
def show(self) -> None:
self.txt.show()
def hide(self) -> None:
self.txt.hide()
def delete(self) -> None:
self.vb.scene().removeItem(self.txt)