Use scientific notation for big values in labeled slider (#226)

* initial implementation

* fix formating labels

* add minimum number of decimals

* fix typo in function name

* add `decimals` method

* fix after napari src migration

* use --import-mode=importlib

* allow enforce decimals

* fix seting 0

* flexible set range for range labels

* better set range

* fix seting mode

* fix max calculation

---------

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
This commit is contained in:
Grzegorz Bokota
2025-06-18 02:20:30 +02:00
committed by GitHub
parent b495c70206
commit 55b66393c3
2 changed files with 120 additions and 36 deletions

View File

@@ -27,11 +27,12 @@ qlds.setValue(0.5)
qlds.setSingleStep(0.1) qlds.setSingleStep(0.1)
qlrs = QLabeledRangeSlider(ORIENTATION) qlrs = QLabeledRangeSlider(ORIENTATION)
qlrs.valueChanged.connect(lambda e: print("QLabeledRangeSlider valueChanged", e)) qlrs.valueChanged.connect(lambda e: print("qlrs valueChanged", e))
qlrs.setValue((20, 60)) qlrs.setRange(0, 10**11)
qlrs.setValue((20, 60 * 10**9))
qldrs = QLabeledDoubleRangeSlider(ORIENTATION) qldrs = QLabeledDoubleRangeSlider(ORIENTATION)
qldrs.valueChanged.connect(lambda e: print("qlrs valueChanged", e)) qldrs.valueChanged.connect(lambda e: print("qldrs valueChanged", e))
qldrs.setRange(0, 1) qldrs.setRange(0, 1)
qldrs.setSingleStep(0.01) qldrs.setSingleStep(0.01)
qldrs.setValue((0.2, 0.7)) qldrs.setValue((0.2, 0.7))

View File

@@ -1,20 +1,18 @@
from __future__ import annotations from __future__ import annotations
import contextlib
from enum import IntEnum, IntFlag, auto from enum import IntEnum, IntFlag, auto
from functools import partial from functools import partial
from typing import TYPE_CHECKING, Any, overload from typing import TYPE_CHECKING, Any, overload
from qtpy import QtGui from qtpy import QtGui
from qtpy.QtCore import Property, QPoint, QSize, Qt, Signal from qtpy.QtCore import Property, QPoint, QSize, Qt, Signal
from qtpy.QtGui import QFontMetrics, QValidator from qtpy.QtGui import QDoubleValidator, QFontMetrics, QValidator
from qtpy.QtWidgets import ( from qtpy.QtWidgets import (
QAbstractSlider, QAbstractSlider,
QBoxLayout, QBoxLayout,
QDoubleSpinBox,
QHBoxLayout, QHBoxLayout,
QLineEdit,
QSlider, QSlider,
QSpinBox,
QStyle, QStyle,
QStyleOptionSpinBox, QStyleOptionSpinBox,
QVBoxLayout, QVBoxLayout,
@@ -660,7 +658,7 @@ class QLabeledDoubleRangeSlider(QLabeledRangeSlider):
"""The color of the bar between the first and last handle.""" """The color of the bar between the first and last handle."""
class SliderLabel(QDoubleSpinBox): class SliderLabel(QLineEdit):
def __init__( def __init__(
self, self,
slider: QSlider, slider: QSlider,
@@ -670,52 +668,139 @@ class SliderLabel(QDoubleSpinBox):
) -> None: ) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self._slider = slider self._slider = slider
self._prefix = ""
self._suffix = ""
self._min = slider.minimum()
self._max = slider.maximum()
self._value = self._min
self._callback = connect
self._decimals = -1
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus) self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.setMode(EdgeLabelMode.LabelIsValue) self.setMode(EdgeLabelMode.LabelIsValue)
self.setDecimals(0) self.setDecimals(0)
self.setText(str(self._value))
validator = QDoubleValidator(self)
validator.setNotation(QDoubleValidator.Notation.ScientificNotation)
self.setValidator(validator)
self.setRange(slider.minimum(), slider.maximum())
slider.rangeChanged.connect(self._update_size) slider.rangeChanged.connect(self._update_size)
self.setAlignment(alignment) self.setAlignment(alignment)
self.setButtonSymbols(QSpinBox.ButtonSymbols.NoButtons)
self.setStyleSheet("background:transparent; border: 0;") self.setStyleSheet("background:transparent; border: 0;")
if connect is not None: if connect is not None:
self.editingFinished.connect(lambda: connect(self.value())) self.editingFinished.connect(self._editing_finished)
self.editingFinished.connect(self._silent_clear_focus) self.editingFinished.connect(self._silent_clear_focus)
self._update_size() self._update_size()
def _editing_finished(self):
self._silent_clear_focus()
self.setValue(float(self.text()))
if self._callback:
self._callback(self.value())
def setRange(self, min_: float, max_: float) -> None:
if self._mode == EdgeLabelMode.LabelIsRange:
max_val = max(abs(min_), abs(max_))
n_digits = max(len(str(int(max_val))), 7)
upper_bound = int("9" * n_digits)
self._min = -upper_bound
self._max = upper_bound
self._update_size()
else:
max_ = max(max_, min_)
self._min = min_
self._max = max_
def setDecimals(self, prec: int) -> None: def setDecimals(self, prec: int) -> None:
super().setDecimals(prec) # super().setDecimals(prec)
self._decimals = prec
self._update_size() self._update_size()
def decimals(self) -> int:
"""Return the number of decimals used in the label."""
return self._decimals
def value(self) -> float:
return self._value
def setValue(self, val: Any) -> None: def setValue(self, val: Any) -> None:
super().setValue(val) if val < self._min:
val = self._min
elif val > self._max:
val = self._max
self._value = val
self.updateText()
def updateText(self) -> None:
val = float(self._value)
use_scientific = (abs(val) < 0.0001 or abs(val) > 9999999.0) and val != 0.0
font_metrics = QFontMetrics(self.font())
eight_len = _fm_width(font_metrics, "8")
available_chars = self.width() // eight_len
total, _fraction = f"{val:.<f}".split(".")
if len(total) > available_chars:
use_scientific = True
if self._decimals < 0:
if use_scientific:
mantissa, exponent = f"{val:.{available_chars}e}".split("e")
mantissa = mantissa.rstrip("0").rstrip(".")
if len(mantissa) + len(exponent) + 1 < available_chars:
text = f"{mantissa}e{exponent}"
else:
decimals = max(available_chars - len(exponent) - 3, 2)
text = f"{val:.{decimals}e}"
else:
decimals = max(available_chars - len(total) - 1, 2)
text = f"{val:.{decimals}f}"
text = text.rstrip("0").rstrip(".")
else:
if use_scientific:
mantissa, exponent = f"{val:.{self._decimals}e}".split("e")
mantissa = mantissa.rstrip("0").rstrip(".")
text = f"{mantissa}e{exponent}"
else:
text = f"{val:.{self._decimals}f}"
if text == "":
text = "0"
self.setText(text)
if self._mode == EdgeLabelMode.LabelIsRange: if self._mode == EdgeLabelMode.LabelIsRange:
self._update_size() self._update_size()
def setMaximum(self, max: float) -> None: def minimum(self):
super().setMaximum(max) return self._min
if self._mode == EdgeLabelMode.LabelIsValue:
self._update_size()
def setMinimum(self, min: float) -> None: def setMaximum(self, max_: float) -> None:
super().setMinimum(min) self.setRange(self._min, max_)
if self._mode == EdgeLabelMode.LabelIsValue:
self._update_size() def maximum(self):
return self._max
def setMinimum(self, min_: float) -> None:
self.setRange(min_, self._max)
def setMode(self, opt: EdgeLabelMode) -> None: def setMode(self, opt: EdgeLabelMode) -> None:
# when the edge labels are controlling slider range, # when the edge labels are controlling slider range,
# we want them to have a big range, but not have a huge label # we want them to have a big range, but not have a huge label
self._mode = opt self._mode = opt
if opt == EdgeLabelMode.LabelIsRange: self.setRange(self._slider.minimum(), self._slider.maximum())
self.setMinimum(-9999999) self._update_size()
self.setMaximum(9999999)
with contextlib.suppress(Exception): def prefix(self) -> str:
self._slider.rangeChanged.disconnect(self.setRange) return self._prefix
else:
self.setMinimum(self._slider.minimum()) def setPrefix(self, prefix: str) -> None:
self.setMaximum(self._slider.maximum()) self._prefix = prefix
self._slider.rangeChanged.connect(self.setRange) self._update_size()
def suffix(self) -> str:
return self._suffix
def setSuffix(self, suffix: str) -> None:
self._suffix = suffix
self._update_size() self._update_size()
# --------------- private ---------------- # --------------- private ----------------
@@ -732,21 +817,19 @@ class SliderLabel(QDoubleSpinBox):
if self._mode & EdgeLabelMode.LabelIsValue: if self._mode & EdgeLabelMode.LabelIsValue:
# determine width based on min/max/specialValue # determine width based on min/max/specialValue
mintext = self.textFromValue(self.minimum())[:18] mintext = str(self.minimum())[:18]
maxtext = self.textFromValue(self.maximum())[:18] maxtext = str(self.maximum())[:18]
w = max(0, _fm_width(fm, mintext + fixed_content)) w = max(0, _fm_width(fm, mintext + fixed_content))
w = max(w, _fm_width(fm, maxtext + fixed_content)) w = max(w, _fm_width(fm, maxtext + fixed_content))
if self.specialValueText():
w = max(w, _fm_width(fm, self.specialValueText()))
if self._mode & EdgeLabelMode.LabelIsRange: if self._mode & EdgeLabelMode.LabelIsRange:
w += 8 # it seems as thought suffix() is not enough w += 8 # it seems as thought suffix() is not enough
else: else:
w = max(0, _fm_width(fm, self.textFromValue(self.value()))) + 3 w = max(0, _fm_width(fm, str(self.value()))) + 3
w += 3 # cursor blinking space w += 3 # cursor blinking space
# get the final size hint # get the final size hint
opt = QStyleOptionSpinBox() opt = QStyleOptionSpinBox()
self.initStyleOption(opt) # self.initStyleOption(opt)
size = self.style().sizeFromContents( size = self.style().sizeFromContents(
QStyle.ContentsType.CT_SpinBox, opt, QSize(w, h), self QStyle.ContentsType.CT_SpinBox, opt, QSize(w, h), self
) )