Add user defined anchor support to label; reorg mod
parent
2d787901f9
commit
2fd7ea812a
|
@ -32,7 +32,6 @@ import trio
|
|||
from ..log import get_logger
|
||||
from ._style import _min_points_to_show
|
||||
from ._editors import SelectRect
|
||||
from ._window import main_window
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
|
|
@ -19,7 +19,7 @@ Non-shitty labels that don't re-invent the wheel.
|
|||
|
||||
"""
|
||||
from inspect import isfunction
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
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]:
|
||||
"""Return a closure which gives the scene x-coordinate for the
|
||||
leftmost point of the containing view box.
|
||||
|
@ -85,170 +279,3 @@ def right_axis(
|
|||
return ryaxis.pos().x() # + axis_offset - 2
|
||||
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue