diff --git a/piker/ui/_label.py b/piker/ui/_label.py new file mode 100644 index 00000000..4704bfed --- /dev/null +++ b/piker/ui/_label.py @@ -0,0 +1,188 @@ +# 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 . + +""" +Non-shitty labels that don't re-invent the wheel. + +""" +from typing import Callable + +import pyqtgraph as pg +from PyQt5 import QtGui +from PyQt5.QtCore import QPointF, QRectF + + +from ._style import ( + DpiAwareFont, + hcolor, + _down_2_font_inches_we_like, +) + + +def vbr_left(label) -> float: + + def viewbox_left(): + return label.vbr().left() + + return viewbox_left + + +def right_axis(chart, label) -> float: + raxis = chart.getAxis('right') + + def right_axis_offset_by_w(): + return raxis.pos().x() - label.w + + return right_axis_offset_by_w + + +class Label: + """ + 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, pieces of garbage that + can't accomplish the simplest things, such as pinning to the left + hand side of a view box. + + This type is another effort (see our graphics lol) to start making + small, re-usable label components that can actually be used to build + production grade UIs. + + just.. smh, hard. + + """ + def __init__( + self, + view: pg.ViewBox, + fmt_str: str, + color: str = 'bracket', + x_offset: float = 0, + font_size_inches: float = _down_2_font_inches_we_like, + 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 = QtGui.QGraphicsTextItem() + vb.scene().addItem(txt) + + # configure font size based on DPI + dpi_font = DpiAwareFont( + size_in_inches=font_size_inches + ) + 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._af = self.txt.pos().x + + # 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._af = func + + def set_view_y( + self, + y: float, + ) -> None: + + scene_x = self._af() 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: + text = self._fmt_str.format(**fields) + 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()