Skip to content

Commit

Permalink
DoubleSpinBox: Add a DoubleSpinBox with non-fixed decimal rounding
Browse files Browse the repository at this point in the history
Implement `stepBy` update using decimal type.
  • Loading branch information
ales-erjavec committed Jan 26, 2021
1 parent 2bfca2c commit ba28128
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 5 deletions.
11 changes: 6 additions & 5 deletions Orange/widgets/data/owimpute.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
from AnyQt.QtWidgets import (
QGroupBox, QRadioButton, QPushButton, QHBoxLayout, QGridLayout,
QVBoxLayout, QStackedWidget, QComboBox, QWidget,
QButtonGroup, QStyledItemDelegate, QListView, QDoubleSpinBox, QLabel
QButtonGroup, QStyledItemDelegate, QListView, QLabel
)
from AnyQt.QtCore import Qt, QThread, QModelIndex, QDateTime
from AnyQt.QtCore import pyqtSlot as Slot

from Orange.widgets.utils.spinbox import DoubleSpinBox
from orangewidget.utils.listview import ListViewSearch

import Orange.data
Expand Down Expand Up @@ -209,8 +210,8 @@ def set_default_time(datetime):
button.setChecked(Method.Default == self.default_method_index)
hlayout.addWidget(button)

self.numeric_value_widget = QDoubleSpinBox(
minimum=DBL_MIN, maximum=DBL_MAX, singleStep=.1, decimals=5,
self.numeric_value_widget = DoubleSpinBox(
minimum=DBL_MIN, maximum=DBL_MAX, singleStep=.1,
value=self.default_numeric_value,
alignment=Qt.AlignRight,
enabled=self.default_method_index == Method.Default,
Expand Down Expand Up @@ -281,9 +282,9 @@ def set_default_time(datetime):
sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,
activated=self._on_value_selected
)
self.value_double = QDoubleSpinBox(
self.value_double = DoubleSpinBox(
editingFinished=self._on_value_selected,
minimum=DBL_MIN, maximum=DBL_MAX, singleStep=.1, decimals=5,
minimum=DBL_MIN, maximum=DBL_MAX, singleStep=.1,
)
self.value_stack = value_stack = QStackedWidget()
value_stack.addWidget(self.value_combo)
Expand Down
109 changes: 109 additions & 0 deletions Orange/widgets/utils/spinbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import math
from decimal import Decimal

import numpy as np

from AnyQt.QtCore import QLocale
from AnyQt.QtWidgets import QDoubleSpinBox

DBL_MIN = float(np.finfo(float).min)
DBL_MAX = float(np.finfo(float).max)
DBL_MAX_10_EXP = math.floor(math.log10(DBL_MAX))
DBL_DIG = math.floor(math.log10(2 ** np.finfo(float).nmant))


class DoubleSpinBox(QDoubleSpinBox):
"""
A QDoubleSpinSubclass with non-fixed decimal precision/rounding.
"""
def __init__(self, parent=None, decimals=-1, minimumStep=1e-5, **kwargs):
self.__decimals = decimals
self.__minimumStep = minimumStep
stepType = kwargs.pop("stepType", DoubleSpinBox.DefaultStepType)
super().__init__(parent, **kwargs)
if decimals < 0:
super().setDecimals(DBL_MAX_10_EXP + DBL_DIG)
else:
super().setDecimals(decimals)
self.setStepType(stepType)

def setDecimals(self, prec: int) -> None:
"""
Set the number of decimals in display/edit
If negative value then no rounding takes place and the value is
displayed using `QLocale.FloatingPointShortest` precision.
"""
self.__decimals = prec
if prec < 0:
# disable rounding in base implementation.
super().setDecimals(DBL_MAX_10_EXP + DBL_DIG)
else:
super().setDecimals(prec)

def decimals(self):
return self.__decimals

def setMinimumStep(self, step):
"""
Minimum step size when `stepType() == AdaptiveDecimalStepType`
and `decimals() < 0`.
"""
self.__minimumStep = step

def minimumStep(self):
return self.__minimumStep

def textFromValue(self, v: float) -> str:
"""Reimplemented."""
if self.__decimals < 0:
locale = self.locale()
return locale.toString(v, 'f', QLocale.FloatingPointShortest)
else:
return super().textFromValue(v)

def stepBy(self, steps: int) -> None:
"""
Reimplemented.
"""
# Compute up/down step using decimal type without rounding
value = self.value()
value_dec = Decimal(str(value))
if self.stepType() == DoubleSpinBox.AdaptiveDecimalStepType:
step_dec = self.__adaptiveDecimalStep(steps)
else:
step_dec = Decimal(str(self.singleStep()))
# print(str(step_dec.fma(steps, value_dec)))
value_dec = value_dec + step_dec * steps
# print(str(value), "+", str(step_dec), "*", steps, "=", str(vinc))
self.setValue(float(value_dec))

def __adaptiveDecimalStep(self, steps: int) -> Decimal:
# adapted from QDoubleSpinBoxPrivate::calculateAdaptiveDecimalStep
decValue: Decimal = Decimal(str(self.value()))
decimals = self.__decimals
if decimals < 0:
minStep = Decimal(str(self.__minimumStep))
else:
minStep = Decimal(10) ** -decimals

absValue = abs(decValue)
if absValue < minStep:
return minStep
valueNegative = decValue < 0
stepsNegative = steps < 0
if valueNegative != stepsNegative:
absValue /= Decimal("1.01")
step = Decimal(10) ** (math.floor(absValue.log10()) - 1)
return max(minStep, step)

if not hasattr(QDoubleSpinBox, "stepType"): # pragma: no cover
DefaultStepType = 0
AdaptiveDecimalStepType = 1
__stepType = AdaptiveDecimalStepType

def setStepType(self, stepType):
self.__stepType = stepType

def stepType(self):
return self.__stepType
45 changes: 45 additions & 0 deletions Orange/widgets/utils/tests/test_spinbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from Orange.widgets.utils.spinbox import DoubleSpinBox
from orangewidget.tests.base import GuiTest


class TestDoubleSpinBox(GuiTest):
def test_double_spin_box(self):
w = DoubleSpinBox(
minimum=-1, maximum=1, value=0, singleStep=0.1, decimals=-1,
minimumStep=1e-7,
)
self.assertEqual(w.minimum(), -1)
self.assertEqual(w.maximum(), 1)
self.assertEqual(w.value(), 0)
self.assertEqual(w.singleStep(), 0.1)
self.assertEqual(w.decimals(), -1)
self.assertEqual(w.minimumStep(), 1e-7)

w.setValue(2)
self.assertEqual(w.value(), 1)
w.setValue(0.999999)
self.assertEqual(w.value(), 0.999999)
w.stepBy(-1)
self.assertEqual(w.value(), 0.899999)
w.stepBy(1)
self.assertEqual(w.value(), 0.999999)
w.stepBy(1)
self.assertEqual(w.value(), 1.0)

w.setStepType(DoubleSpinBox.AdaptiveDecimalStepType)
w.stepBy(-1)
self.assertEqual(w.value(), 0.99)

w.setValue(0.123456789)
w.stepBy(1)
self.assertEqual(w.value(), 0.133456789)
w.stepBy(-1)
self.assertEqual(w.value(), 0.123456789)
w.setMinimumStep(0.001)
w.setValue(0.00005)
w.stepBy(1)
w.setValue(0.00105)
w.setDecimals(3)
self.assertEqual(w.value(), 0.001)
w.stepBy(1)
self.assertEqual(w.value(), 0.002)

0 comments on commit ba28128

Please sign in to comment.