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 ..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__)
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
Loading…
Reference in New Issue