Skip to content

Commit 55b6639

Browse files
Czakitlambert03
andauthored
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>
1 parent b495c70 commit 55b6639

File tree

2 files changed

+120
-36
lines changed

2 files changed

+120
-36
lines changed

examples/labeled_sliders.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@
2727
qlds.setSingleStep(0.1)
2828

2929
qlrs = QLabeledRangeSlider(ORIENTATION)
30-
qlrs.valueChanged.connect(lambda e: print("QLabeledRangeSlider valueChanged", e))
31-
qlrs.setValue((20, 60))
30+
qlrs.valueChanged.connect(lambda e: print("qlrs valueChanged", e))
31+
qlrs.setRange(0, 10**11)
32+
qlrs.setValue((20, 60 * 10**9))
3233

3334
qldrs = QLabeledDoubleRangeSlider(ORIENTATION)
34-
qldrs.valueChanged.connect(lambda e: print("qlrs valueChanged", e))
35+
qldrs.valueChanged.connect(lambda e: print("qldrs valueChanged", e))
3536
qldrs.setRange(0, 1)
3637
qldrs.setSingleStep(0.01)
3738
qldrs.setValue((0.2, 0.7))

src/superqt/sliders/_labeled.py

Lines changed: 116 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
from __future__ import annotations
22

3-
import contextlib
43
from enum import IntEnum, IntFlag, auto
54
from functools import partial
65
from typing import TYPE_CHECKING, Any, overload
76

87
from qtpy import QtGui
98
from qtpy.QtCore import Property, QPoint, QSize, Qt, Signal
10-
from qtpy.QtGui import QFontMetrics, QValidator
9+
from qtpy.QtGui import QDoubleValidator, QFontMetrics, QValidator
1110
from qtpy.QtWidgets import (
1211
QAbstractSlider,
1312
QBoxLayout,
14-
QDoubleSpinBox,
1513
QHBoxLayout,
14+
QLineEdit,
1615
QSlider,
17-
QSpinBox,
1816
QStyle,
1917
QStyleOptionSpinBox,
2018
QVBoxLayout,
@@ -660,7 +658,7 @@ def _setBarColor(self, color: str) -> None:
660658
"""The color of the bar between the first and last handle."""
661659

662660

663-
class SliderLabel(QDoubleSpinBox):
661+
class SliderLabel(QLineEdit):
664662
def __init__(
665663
self,
666664
slider: QSlider,
@@ -670,52 +668,139 @@ def __init__(
670668
) -> None:
671669
super().__init__(parent=parent)
672670
self._slider = slider
671+
self._prefix = ""
672+
self._suffix = ""
673+
self._min = slider.minimum()
674+
self._max = slider.maximum()
675+
self._value = self._min
676+
self._callback = connect
677+
self._decimals = -1
673678
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
674679
self.setMode(EdgeLabelMode.LabelIsValue)
675680
self.setDecimals(0)
681+
self.setText(str(self._value))
682+
validator = QDoubleValidator(self)
683+
validator.setNotation(QDoubleValidator.Notation.ScientificNotation)
684+
self.setValidator(validator)
676685

677-
self.setRange(slider.minimum(), slider.maximum())
678686
slider.rangeChanged.connect(self._update_size)
679687
self.setAlignment(alignment)
680-
self.setButtonSymbols(QSpinBox.ButtonSymbols.NoButtons)
681688
self.setStyleSheet("background:transparent; border: 0;")
682689
if connect is not None:
683-
self.editingFinished.connect(lambda: connect(self.value()))
690+
self.editingFinished.connect(self._editing_finished)
684691
self.editingFinished.connect(self._silent_clear_focus)
685692
self._update_size()
686693

694+
def _editing_finished(self):
695+
self._silent_clear_focus()
696+
self.setValue(float(self.text()))
697+
if self._callback:
698+
self._callback(self.value())
699+
700+
def setRange(self, min_: float, max_: float) -> None:
701+
if self._mode == EdgeLabelMode.LabelIsRange:
702+
max_val = max(abs(min_), abs(max_))
703+
n_digits = max(len(str(int(max_val))), 7)
704+
upper_bound = int("9" * n_digits)
705+
self._min = -upper_bound
706+
self._max = upper_bound
707+
self._update_size()
708+
else:
709+
max_ = max(max_, min_)
710+
self._min = min_
711+
self._max = max_
712+
687713
def setDecimals(self, prec: int) -> None:
688-
super().setDecimals(prec)
714+
# super().setDecimals(prec)
715+
self._decimals = prec
689716
self._update_size()
690717

718+
def decimals(self) -> int:
719+
"""Return the number of decimals used in the label."""
720+
return self._decimals
721+
722+
def value(self) -> float:
723+
return self._value
724+
691725
def setValue(self, val: Any) -> None:
692-
super().setValue(val)
726+
if val < self._min:
727+
val = self._min
728+
elif val > self._max:
729+
val = self._max
730+
self._value = val
731+
self.updateText()
732+
733+
def updateText(self) -> None:
734+
val = float(self._value)
735+
use_scientific = (abs(val) < 0.0001 or abs(val) > 9999999.0) and val != 0.0
736+
font_metrics = QFontMetrics(self.font())
737+
eight_len = _fm_width(font_metrics, "8")
738+
739+
available_chars = self.width() // eight_len
740+
741+
total, _fraction = f"{val:.<f}".split(".")
742+
743+
if len(total) > available_chars:
744+
use_scientific = True
745+
746+
if self._decimals < 0:
747+
if use_scientific:
748+
mantissa, exponent = f"{val:.{available_chars}e}".split("e")
749+
mantissa = mantissa.rstrip("0").rstrip(".")
750+
if len(mantissa) + len(exponent) + 1 < available_chars:
751+
text = f"{mantissa}e{exponent}"
752+
else:
753+
decimals = max(available_chars - len(exponent) - 3, 2)
754+
text = f"{val:.{decimals}e}"
755+
756+
else:
757+
decimals = max(available_chars - len(total) - 1, 2)
758+
text = f"{val:.{decimals}f}"
759+
text = text.rstrip("0").rstrip(".")
760+
else:
761+
if use_scientific:
762+
mantissa, exponent = f"{val:.{self._decimals}e}".split("e")
763+
mantissa = mantissa.rstrip("0").rstrip(".")
764+
text = f"{mantissa}e{exponent}"
765+
else:
766+
text = f"{val:.{self._decimals}f}"
767+
if text == "":
768+
text = "0"
769+
self.setText(text)
693770
if self._mode == EdgeLabelMode.LabelIsRange:
694771
self._update_size()
695772

696-
def setMaximum(self, max: float) -> None:
697-
super().setMaximum(max)
698-
if self._mode == EdgeLabelMode.LabelIsValue:
699-
self._update_size()
773+
def minimum(self):
774+
return self._min
700775

701-
def setMinimum(self, min: float) -> None:
702-
super().setMinimum(min)
703-
if self._mode == EdgeLabelMode.LabelIsValue:
704-
self._update_size()
776+
def setMaximum(self, max_: float) -> None:
777+
self.setRange(self._min, max_)
778+
779+
def maximum(self):
780+
return self._max
781+
782+
def setMinimum(self, min_: float) -> None:
783+
self.setRange(min_, self._max)
705784

706785
def setMode(self, opt: EdgeLabelMode) -> None:
707786
# when the edge labels are controlling slider range,
708787
# we want them to have a big range, but not have a huge label
709788
self._mode = opt
710-
if opt == EdgeLabelMode.LabelIsRange:
711-
self.setMinimum(-9999999)
712-
self.setMaximum(9999999)
713-
with contextlib.suppress(Exception):
714-
self._slider.rangeChanged.disconnect(self.setRange)
715-
else:
716-
self.setMinimum(self._slider.minimum())
717-
self.setMaximum(self._slider.maximum())
718-
self._slider.rangeChanged.connect(self.setRange)
789+
self.setRange(self._slider.minimum(), self._slider.maximum())
790+
self._update_size()
791+
792+
def prefix(self) -> str:
793+
return self._prefix
794+
795+
def setPrefix(self, prefix: str) -> None:
796+
self._prefix = prefix
797+
self._update_size()
798+
799+
def suffix(self) -> str:
800+
return self._suffix
801+
802+
def setSuffix(self, suffix: str) -> None:
803+
self._suffix = suffix
719804
self._update_size()
720805

721806
# --------------- private ----------------
@@ -732,21 +817,19 @@ def _update_size(self, *_: Any) -> None:
732817

733818
if self._mode & EdgeLabelMode.LabelIsValue:
734819
# determine width based on min/max/specialValue
735-
mintext = self.textFromValue(self.minimum())[:18]
736-
maxtext = self.textFromValue(self.maximum())[:18]
820+
mintext = str(self.minimum())[:18]
821+
maxtext = str(self.maximum())[:18]
737822
w = max(0, _fm_width(fm, mintext + fixed_content))
738823
w = max(w, _fm_width(fm, maxtext + fixed_content))
739-
if self.specialValueText():
740-
w = max(w, _fm_width(fm, self.specialValueText()))
741824
if self._mode & EdgeLabelMode.LabelIsRange:
742825
w += 8 # it seems as thought suffix() is not enough
743826
else:
744-
w = max(0, _fm_width(fm, self.textFromValue(self.value()))) + 3
827+
w = max(0, _fm_width(fm, str(self.value()))) + 3
745828

746829
w += 3 # cursor blinking space
747830
# get the final size hint
748831
opt = QStyleOptionSpinBox()
749-
self.initStyleOption(opt)
832+
# self.initStyleOption(opt)
750833
size = self.style().sizeFromContents(
751834
QStyle.ContentsType.CT_SpinBox, opt, QSize(w, h), self
752835
)

0 commit comments

Comments
 (0)