piker/piker/ui/_label.py

291 lines
7.5 KiB
Python
Raw Normal View History

2021-02-11 05:12:37 +00:00
# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
# 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/>.
"""
Non-shitty labels that don't re-invent the wheel.
"""
from inspect import isfunction
from typing import Callable, Optional, Any
2021-02-11 05:12:37 +00:00
import pyqtgraph as pg
2021-07-21 19:50:09 +00:00
from PyQt5 import QtGui, QtWidgets
2021-09-16 13:17:44 +00:00
from PyQt5.QtWidgets import QLabel, QSizePolicy
from PyQt5.QtCore import QPointF, QRectF, Qt
2021-02-11 05:12:37 +00:00
from ._style import (
DpiAwareFont,
hcolor,
_font,
2021-02-11 05:12:37 +00:00
)
class Label:
2022-01-21 13:37:31 +00:00
'''
2021-06-23 14:06:27 +00:00
A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
2021-02-11 05:12:37 +00:00
After hacking for many days on multiple "label" systems inside
``pyqtgraph`` yet again we're left writing our own since it seems
2021-06-23 14:06:27 +00:00
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.
2021-02-11 05:12:37 +00:00
This type is another effort (see our graphics) to start making
2021-02-11 05:12:37 +00:00
small, re-usable label components that can actually be used to build
production grade UIs...
2021-02-11 05:12:37 +00:00
2022-01-21 13:37:31 +00:00
'''
2021-02-11 05:12:37 +00:00
def __init__(
self,
view: pg.ViewBox,
fmt_str: str,
color: str = 'default_light',
2021-02-11 05:12:37 +00:00
x_offset: float = 0,
font_size: str = 'small',
opacity: float = 1,
fields: dict = {},
2022-01-21 13:37:31 +00:00
parent: pg.GraphicsObject = None,
update_on_range_change: bool = True,
2021-02-11 05:12:37 +00:00
) -> None:
vb = self.vb = view
self._fmt_str = fmt_str
self._view_xy = QPointF(0, 0)
2022-01-21 13:37:31 +00:00
self.scene_anchor: Optional[
Callable[..., QPointF]
] = None
2021-02-11 05:12:37 +00:00
self._x_offset = x_offset
2022-01-21 13:37:31 +00:00
txt = self.txt = QtWidgets.QGraphicsTextItem(parent=parent)
2021-08-29 20:01:51 +00:00
txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
2021-02-11 05:12:37 +00:00
vb.scene().addItem(txt)
# configure font size based on DPI
dpi_font = DpiAwareFont(
font_size=font_size,
2021-02-11 05:12:37 +00:00
)
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)
2021-02-11 05:12:37 +00:00
self._hcolor: str = ''
self.color = color
self.fields = fields
self.orient_v = 'bottom'
2021-02-24 17:05:09 +00:00
self._anchor_func = self.txt.pos().x
2021-02-11 05:12:37 +00:00
2021-02-11 16:49:01 +00:00
# not sure if this makes a diff
2021-07-21 19:50:09 +00:00
self.txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
2021-02-11 16:49:01 +00:00
2021-02-11 05:12:37 +00:00
# TODO: edit and selection support
# https://doc.qt.io/qt-5/qt.html#TextInteractionFlag-enum
# self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
@property
2022-01-21 13:37:31 +00:00
def color(self) -> str:
2021-02-11 05:12:37 +00:00
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:
2022-01-21 13:37:31 +00:00
'''
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())
2021-02-11 05:12:37 +00:00
def on_sigrange_change(self, vr, r) -> None:
return self.update()
2021-02-11 05:12:37 +00:00
@property
def w(self) -> float:
return self.txt.boundingRect().width()
def scene_br(self) -> QRectF:
txt = self.txt
return txt.mapToScene(
txt.boundingRect()
).boundingRect()
2021-02-11 05:12:37 +00:00
@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)
2021-02-24 17:05:09 +00:00
self._anchor_func = func
2021-02-11 05:12:37 +00:00
def set_view_pos(
2021-02-11 05:12:37 +00:00
self,
2021-02-11 05:12:37 +00:00
y: float,
x: Optional[float] = None,
2021-02-11 05:12:37 +00:00
) -> 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()
2021-02-11 05:12:37 +00:00
# get new (inside the) view coordinates / position
self._view_xy = QPointF(x, y)
2021-02-11 05:12:37 +00:00
# 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()
@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(',', ' ')
2021-02-11 05:12:37 +00:00
self.txt.setPlainText(text)
def render(self) -> None:
self.format(**self.fields)
def show(self) -> None:
self.txt.show()
2021-09-10 15:33:58 +00:00
self.txt.update()
2021-02-11 05:12:37 +00:00
def hide(self) -> None:
self.txt.hide()
2021-02-11 16:49:01 +00:00
def delete(self) -> None:
self.vb.scene().removeItem(self.txt)
class FormatLabel(QLabel):
2022-01-21 13:37:31 +00:00
'''
Kinda similar to above but using the widget apis.
'''
def __init__(
self,
fmt_str: str,
font: QtGui.QFont,
font_size: int,
font_color: str,
parent=None,
) -> None:
super().__init__(parent)
# by default set the format string verbatim and expect user to
# call ``.format()`` later (presumably they'll notice the
# unformatted content if ``fmt_str`` isn't meant to be
# unformatted).
self.fmt_str = fmt_str
self.setText(fmt_str)
self.setStyleSheet(
f"""QLabel {{
color : {hcolor(font_color)};
font-size : {font_size}px;
}}
"""
)
self.setFont(_font.font)
self.setTextFormat(Qt.MarkdownText) # markdown
self.setMargin(0)
2021-09-16 13:17:44 +00:00
self.setSizePolicy(
QSizePolicy.Expanding,
QSizePolicy.Expanding,
)
2022-01-21 13:37:31 +00:00
self.setAlignment(
Qt.AlignVCenter | Qt.AlignLeft
)
self.setText(self.fmt_str)
def format(
self,
2021-08-26 14:34:14 +00:00
**fields: dict[str, Any],
) -> str:
out = self.fmt_str.format(**fields)
self.setText(out)
return out