Skip to content

Use scientific notation for big values in labeled slider #226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
uses: pyapp-kit/workflows/.github/workflows/upload-coverage.yml@v2
secrets: inherit

test_napari:
test_napari_old:
uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v2
with:
dependency-repo: napari/napari
Expand All @@ -100,9 +100,25 @@ jobs:
strategy:
fail-fast: false
matrix:
napari-version: ["", "v0.4.19.post1"]
napari-version: ["v0.4.19.post1"]
qt: ["pyqt5", "pyside2"]

test_napari:
uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v2
with:
dependency-repo: napari/napari
dependency-ref: ${{ matrix.napari-version }}
dependency-extras: "testing"
qt: ${{ matrix.qt }}
pytest-args: 'src/napari/_qt --import-mode=importlib -k "not async and not qt_dims_2 and not qt_viewer_console_focus and not keybinding_editor and not preferences_dialog_not_dismissed"'
python-version: "3.10"
post-install-cmd: "pip install lxml_html_clean"
strategy:
fail-fast: false
matrix:
napari-version: [ "" ]
qt: [ "pyqt5", "pyside2" ]

check-manifest:
name: Check Manifest
runs-on: ubuntu-latest
Expand Down
7 changes: 4 additions & 3 deletions examples/labeled_sliders.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@
qlds.setSingleStep(0.1)

qlrs = QLabeledRangeSlider(ORIENTATION)
qlrs.valueChanged.connect(lambda e: print("QLabeledRangeSlider valueChanged", e))
qlrs.setValue((20, 60))
qlrs.valueChanged.connect(lambda e: print("qlrs valueChanged", e))
qlrs.setRange(0, 10**11)
qlrs.setValue((20, 60 * 10**9))

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.setSingleStep(0.01)
qldrs.setValue((0.2, 0.7))
Expand Down
149 changes: 116 additions & 33 deletions src/superqt/sliders/_labeled.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
from __future__ import annotations

import contextlib
from enum import IntEnum, IntFlag, auto
from functools import partial
from typing import TYPE_CHECKING, Any, overload

from qtpy import QtGui
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 (
QAbstractSlider,
QBoxLayout,
QDoubleSpinBox,
QHBoxLayout,
QLineEdit,
QSlider,
QSpinBox,
QStyle,
QStyleOptionSpinBox,
QVBoxLayout,
Expand Down Expand Up @@ -629,7 +627,7 @@
"""The color of the bar between the first and last handle."""


class SliderLabel(QDoubleSpinBox):
class SliderLabel(QLineEdit):
def __init__(
self,
slider: QSlider,
Expand All @@ -639,52 +637,139 @@
) -> None:
super().__init__(parent=parent)
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.setMode(EdgeLabelMode.LabelIsValue)
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)
self.setAlignment(alignment)
self.setButtonSymbols(QSpinBox.ButtonSymbols.NoButtons)
self.setStyleSheet("background:transparent; border: 0;")
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._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:
super().setDecimals(prec)
# super().setDecimals(prec)
self._decimals = prec
self._update_size()

def decimals(self) -> int:
"""Return the number of decimals used in the label."""
return self._decimals

Check warning on line 689 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L689

Added line #L689 was not covered by tests

def value(self) -> float:
return self._value

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}"

Check warning on line 720 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L716-L720

Added lines #L716 - L720 were not covered by tests
else:
decimals = max(available_chars - len(exponent) - 3, 2)
text = f"{val:.{decimals}e}"

Check warning on line 723 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L722-L723

Added lines #L722 - L723 were not covered by tests

else:
decimals = max(available_chars - len(total) - 1, 2)
text = f"{val:.{decimals}f}"
text = text.rstrip("0").rstrip(".")

Check warning on line 728 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L726-L728

Added lines #L726 - L728 were not covered by tests
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"

Check warning on line 737 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L737

Added line #L737 was not covered by tests
self.setText(text)
if self._mode == EdgeLabelMode.LabelIsRange:
self._update_size()

def setMaximum(self, max: float) -> None:
super().setMaximum(max)
if self._mode == EdgeLabelMode.LabelIsValue:
self._update_size()
def minimum(self):
return self._min

def setMinimum(self, min: float) -> None:
super().setMinimum(min)
if self._mode == EdgeLabelMode.LabelIsValue:
self._update_size()
def setMaximum(self, max_: float) -> None:
self.setRange(self._min, max_)

Check warning on line 746 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L746

Added line #L746 was not covered by tests

def maximum(self):
return self._max

def setMinimum(self, min_: float) -> None:
self.setRange(min_, self._max)

Check warning on line 752 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L752

Added line #L752 was not covered by tests

def setMode(self, opt: EdgeLabelMode) -> None:
# when the edge labels are controlling slider range,
# we want them to have a big range, but not have a huge label
self._mode = opt
if opt == EdgeLabelMode.LabelIsRange:
self.setMinimum(-9999999)
self.setMaximum(9999999)
with contextlib.suppress(Exception):
self._slider.rangeChanged.disconnect(self.setRange)
else:
self.setMinimum(self._slider.minimum())
self.setMaximum(self._slider.maximum())
self._slider.rangeChanged.connect(self.setRange)
self.setRange(self._slider.minimum(), self._slider.maximum())
self._update_size()

def prefix(self) -> str:
return self._prefix

def setPrefix(self, prefix: str) -> None:
self._prefix = prefix
self._update_size()

Check warning on line 766 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L765-L766

Added lines #L765 - L766 were not covered by tests

def suffix(self) -> str:
return self._suffix

def setSuffix(self, suffix: str) -> None:
self._suffix = suffix

Check warning on line 772 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L772

Added line #L772 was not covered by tests
self._update_size()

# --------------- private ----------------
Expand All @@ -701,21 +786,19 @@

if self._mode & EdgeLabelMode.LabelIsValue:
# determine width based on min/max/specialValue
mintext = self.textFromValue(self.minimum())[:18]
maxtext = self.textFromValue(self.maximum())[:18]
mintext = str(self.minimum())[:18]
maxtext = str(self.maximum())[:18]
w = max(0, _fm_width(fm, mintext + 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:
w += 8 # it seems as thought suffix() is not enough
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
# get the final size hint
opt = QStyleOptionSpinBox()
self.initStyleOption(opt)
# self.initStyleOption(opt)
size = self.style().sizeFromContents(
QStyle.ContentsType.CT_SpinBox, opt, QSize(w, h), self
)
Expand Down