-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from jnsebgosselin/add_range_widget
PR: Add range spinbox and range widget classes
- Loading branch information
Showing
4 changed files
with
357 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# -*- coding: utf-8 -*- | ||
# ----------------------------------------------------------------------------- | ||
# Copyright © QtAppUtils Project Contributors | ||
# https://github.com/jnsebgosselin/apputils | ||
# | ||
# This file is part of QtAppUtils. | ||
# Licensed under the terms of the MIT License. | ||
# ----------------------------------------------------------------------------- | ||
|
||
from .range import RangeSpinBox, RangeWidget |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
# -*- coding: utf-8 -*- | ||
# ----------------------------------------------------------------------------- | ||
# Copyright © QtAppUtils Project Contributors | ||
# https://github.com/jnsebgosselin/apputils | ||
# | ||
# This file is part of QtAppUtils. | ||
# Licensed under the terms of the MIT License. | ||
# ----------------------------------------------------------------------------- | ||
|
||
# ---- Third party imports | ||
from qtpy.QtCore import Signal, QObject | ||
from qtpy.QtGui import QValidator | ||
from qtpy.QtWidgets import QDoubleSpinBox, QWidget | ||
|
||
|
||
class RangeSpinBox(QDoubleSpinBox): | ||
""" | ||
A spinbox that allow to enter values that are lower or higher than the | ||
minimum and maximum value of the spinbox. | ||
When editing is finished, the value is corrected to the maximum if the | ||
value entered was above the maximum and to the minimum if it was below | ||
the minimum. | ||
""" | ||
|
||
def __init__(self, parent: QWidget = None, maximum: float = None, | ||
minimum: float = None, singlestep: float = None, | ||
decimals: int = None, value: float = None): | ||
super().__init__(parent) | ||
self.setKeyboardTracking(False) | ||
if minimum is not None: | ||
self.setMinimum(minimum) | ||
if maximum is not None: | ||
self.setMaximum(maximum) | ||
if singlestep is not None: | ||
self.setSingleStep(singlestep) | ||
if decimals is not None: | ||
self.setDecimals(decimals) | ||
if value is not None: | ||
self.setValue(value) | ||
|
||
def sizeHint(self): | ||
""" | ||
Override Qt method to add a buffer to the hint width of the spinbox, | ||
so that there is enough space to hold the maximum value in the spinbox | ||
when editing. | ||
""" | ||
qsize = super().sizeHint() | ||
qsize.setWidth(qsize.width() + 8) | ||
return qsize | ||
|
||
def fixup(self, value): | ||
"""Override Qt method.""" | ||
if value in ('-', '', '.', ','): | ||
return super().fixup(value) | ||
elif float(value) > self.maximum(): | ||
return '{value:0.{decimals}f}'.format( | ||
value=self.maximum(), decimals=self.decimals()) | ||
else: | ||
return '{value:0.{decimals}f}'.format( | ||
value=self.minimum(), decimals=self.decimals()) | ||
|
||
def validate(self, value, pos): | ||
"""Override Qt method.""" | ||
if value in ('-', '', '.', ','): | ||
return QValidator.Intermediate, value, pos | ||
|
||
try: | ||
float(value) | ||
except(ValueError): | ||
return QValidator.Invalid, value, pos | ||
|
||
if float(value) > self.maximum() or float(value) < self.minimum(): | ||
return QValidator.Intermediate, value, pos | ||
else: | ||
return QValidator.Acceptable, value, pos | ||
|
||
|
||
class RangeWidget(QObject): | ||
""" | ||
A Qt object that link two double spinboxes that can be used to define | ||
the start and end value of a range. | ||
The RangeWidget does not come with a layout and both the spinbox_start | ||
and spinbox_end must be added to a layout independently. | ||
""" | ||
sig_range_changed = Signal(float, float) | ||
|
||
def __init__(self, parent: QWidget = None, maximum: float = 99.99, | ||
minimum: float = 0, singlestep: float = 0.01, | ||
decimals: int = 2, null_range_ok: bool = True): | ||
super().__init__() | ||
self.decimals = decimals | ||
self.null_range_ok = null_range_ok | ||
|
||
self.spinbox_start = RangeSpinBox( | ||
minimum=minimum, singlestep=singlestep, decimals=decimals, | ||
value=minimum) | ||
self.spinbox_start.valueChanged.connect( | ||
lambda: self._handle_value_changed()) | ||
self.spinbox_start.editingFinished.connect( | ||
lambda: self._handle_value_changed()) | ||
|
||
self.spinbox_end = RangeSpinBox( | ||
maximum=maximum, singlestep=singlestep, decimals=decimals, | ||
value=maximum) | ||
self.spinbox_end.valueChanged.connect( | ||
lambda: self._handle_value_changed()) | ||
self.spinbox_end.editingFinished.connect( | ||
lambda: self._handle_value_changed()) | ||
|
||
self._update_spinbox_range() | ||
|
||
def start(self): | ||
"""Return the start value of the range.""" | ||
return self.spinbox_start.value() | ||
|
||
def end(self): | ||
"""Return the end value of the range.""" | ||
return self.spinbox_end.value() | ||
|
||
def set_range(self, start: float, end: float): | ||
"""Set the start and end value of the range.""" | ||
old_start = self.start() | ||
old_end = self.end() | ||
|
||
self._block_spinboxes_signals(True) | ||
self.spinbox_start.setValue(start) | ||
self.spinbox_end.setValue(end) | ||
self._block_spinboxes_signals(False) | ||
|
||
if old_start != self.start() or old_end != self.end(): | ||
self._handle_value_changed() | ||
|
||
def set_minimum(self, minimum: float): | ||
"""Set the minimum allowed value of the start of the range.""" | ||
old_start = self.start() | ||
|
||
self._block_spinboxes_signals(True) | ||
self.spinbox_start.setMinimum(minimum) | ||
self._block_spinboxes_signals(False) | ||
|
||
if old_start != self.start(): | ||
self._handle_value_changed() | ||
|
||
def set_maximum(self, maximum: float): | ||
"""Set the maximum allowed value of the end of the range.""" | ||
old_end = self.end() | ||
|
||
self._block_spinboxes_signals(True) | ||
self.spinbox_end.setMaximum(maximum) | ||
self._block_spinboxes_signals(False) | ||
|
||
if old_end != self.end(): | ||
self._handle_value_changed() | ||
|
||
# ---- Private methods and handlers | ||
def _block_spinboxes_signals(self, block: bool): | ||
"""Block signals from the spinboxes.""" | ||
self.spinbox_start.blockSignals(block) | ||
self.spinbox_end.blockSignals(block) | ||
|
||
def _update_spinbox_range(self): | ||
""" | ||
Make sure lowcut and highcut spinbox ranges are | ||
mutually exclusive | ||
""" | ||
self._block_spinboxes_signals(True) | ||
step = 0 if self.null_range_ok else 10**-self.decimals | ||
self.spinbox_start.setMaximum(self.spinbox_end.value() - step) | ||
self.spinbox_end.setMinimum(self.spinbox_start.value() + step) | ||
self._block_spinboxes_signals(False) | ||
|
||
def _handle_value_changed(self, silent: bool = False): | ||
""" | ||
Handle when the value of the lowcut or highcut spinbox changed. | ||
""" | ||
self._update_spinbox_range() | ||
self.sig_range_changed.emit(self.start(), self.end()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# -*- coding: utf-8 -*- | ||
# ----------------------------------------------------------------------------- | ||
# Copyright © QtAppUtils Project Contributors | ||
# https://github.com/jnsebgosselin/apputils | ||
# | ||
# This file is part of QtAppUtils. | ||
# Licensed under the terms of the MIT License. | ||
# ----------------------------------------------------------------------------- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
# -*- coding: utf-8 -*- | ||
# ----------------------------------------------------------------------------- | ||
# Copyright © QtAppUtils Project Contributors | ||
# https://github.com/jnsebgosselin/apputils | ||
# | ||
# This file is part of QtAppUtils. | ||
# Licensed under the terms of the MIT License. | ||
# ----------------------------------------------------------------------------- | ||
|
||
"""Tests for the RangeSpinBox and RangeWidget.""" | ||
|
||
from qtpy.QtCore import Qt | ||
from qtapputils.widgets import RangeSpinBox, RangeWidget | ||
import pytest | ||
|
||
|
||
# ============================================================================= | ||
# Fixtures | ||
# ============================================================================= | ||
@pytest.fixture | ||
def range_spinbox(qtbot): | ||
spinbox = RangeSpinBox( | ||
minimum=3, maximum=101, singlestep=0.1, decimals=2, value=74.21) | ||
qtbot.addWidget(spinbox) | ||
spinbox.show() | ||
|
||
assert spinbox.maximum() == 101 | ||
assert spinbox.minimum() == 3 | ||
assert spinbox.decimals() == 2 | ||
assert spinbox.singleStep() == 0.1 | ||
assert spinbox.value() == 74.21 | ||
|
||
return spinbox | ||
|
||
|
||
@pytest.fixture | ||
def range_widget(qtbot): | ||
widget = RangeWidget(null_range_ok=False) | ||
qtbot.addWidget(widget.spinbox_start) | ||
qtbot.addWidget(widget.spinbox_end) | ||
widget.spinbox_start.show() | ||
widget.spinbox_end.show() | ||
|
||
assert widget.start() == 0 | ||
assert widget.end() == 99.99 | ||
|
||
assert widget.spinbox_start.minimum() == 0 | ||
assert widget.spinbox_start.maximum() == 99.98 | ||
assert widget.spinbox_start.decimals() == 2 | ||
assert widget.spinbox_start.singleStep() == 0.01 | ||
assert widget.spinbox_start.value() == 0 | ||
|
||
assert widget.spinbox_end.minimum() == 0.01 | ||
assert widget.spinbox_end.maximum() == 99.99 | ||
assert widget.spinbox_end.decimals() == 2 | ||
assert widget.spinbox_end.singleStep() == 0.01 | ||
assert widget.spinbox_end.value() == 99.99 | ||
|
||
return widget | ||
|
||
|
||
# ============================================================================= | ||
# Tests | ||
# ============================================================================= | ||
def test_range_spinbox(range_spinbox, qtbot): | ||
""" | ||
Test that the RangeSpinBox is working as expected. | ||
""" | ||
# Test entering a value above the maximum. | ||
range_spinbox.clear() | ||
qtbot.keyClicks(range_spinbox, '120') | ||
qtbot.keyClick(range_spinbox, Qt.Key_Enter) | ||
assert range_spinbox.value() == 101 | ||
|
||
# Test entering a value below the minimum. | ||
range_spinbox.clear() | ||
qtbot.keyClicks(range_spinbox, '-12') | ||
qtbot.keyClick(range_spinbox, Qt.Key_Enter) | ||
assert range_spinbox.value() == 3 | ||
|
||
# Test entering a valid value. | ||
range_spinbox.clear() | ||
qtbot.keyClicks(range_spinbox, '45.3') | ||
qtbot.keyClick(range_spinbox, Qt.Key_Enter) | ||
assert range_spinbox.value() == 45.3 | ||
|
||
# Test entering an intermediate value. | ||
range_spinbox.clear() | ||
qtbot.keyClicks(range_spinbox, '-') | ||
qtbot.keyClick(range_spinbox, Qt.Key_Enter) | ||
assert range_spinbox.value() == 45.3 | ||
|
||
# Test entering invalid values. | ||
range_spinbox.clear() | ||
qtbot.keyClicks(range_spinbox, '23..a-45') | ||
qtbot.keyClick(range_spinbox, Qt.Key_Enter) | ||
assert range_spinbox.value() == 23.45 | ||
|
||
|
||
def test_range_widget(range_widget, qtbot): | ||
""" | ||
Test that the RangeWidget is working as expected. | ||
""" | ||
# Test set_minimum functionality. | ||
range_widget.set_minimum(24) | ||
assert range_widget.start() == 24 | ||
assert range_widget.end() == 99.99 | ||
|
||
assert range_widget.spinbox_start.minimum() == 24 | ||
assert range_widget.spinbox_start.maximum() == 99.98 | ||
assert range_widget.spinbox_start.value() == 24 | ||
|
||
assert range_widget.spinbox_end.minimum() == 24.01 | ||
assert range_widget.spinbox_end.maximum() == 99.99 | ||
assert range_widget.spinbox_end.value() == 99.99 | ||
|
||
# Test set_maximum functionality. | ||
range_widget.set_maximum(74) | ||
assert range_widget.start() == 24 | ||
assert range_widget.end() == 74 | ||
|
||
assert range_widget.spinbox_start.minimum() == 24 | ||
assert range_widget.spinbox_start.maximum() == 73.99 | ||
assert range_widget.spinbox_start.value() == 24 | ||
|
||
assert range_widget.spinbox_end.minimum() == 24.01 | ||
assert range_widget.spinbox_end.maximum() == 74.00 | ||
assert range_widget.spinbox_end.value() == 74.00 | ||
|
||
# Test set_range functionality. | ||
range_widget.set_range(48, 62.3) | ||
assert range_widget.start() == 48 | ||
assert range_widget.end() == 62.3 | ||
|
||
assert range_widget.spinbox_start.minimum() == 24 | ||
assert range_widget.spinbox_start.maximum() == 62.29 | ||
assert range_widget.spinbox_start.value() == 48 | ||
|
||
assert range_widget.spinbox_end.minimum() == 48.01 | ||
assert range_widget.spinbox_end.maximum() == 74.00 | ||
assert range_widget.spinbox_end.value() == 62.3 | ||
|
||
# Test entering values in spinbox. | ||
range_widget.spinbox_start.clear() | ||
qtbot.keyClicks(range_widget.spinbox_start, '43.51') | ||
qtbot.keyClick(range_widget.spinbox_start, Qt.Key_Enter) | ||
assert range_widget.start() == 43.51 | ||
assert range_widget.end() == 62.3 | ||
|
||
assert range_widget.spinbox_start.minimum() == 24 | ||
assert range_widget.spinbox_start.maximum() == 62.29 | ||
assert range_widget.spinbox_start.value() == 43.51 | ||
|
||
assert range_widget.spinbox_end.minimum() == 43.52 | ||
assert range_widget.spinbox_end.maximum() == 74.00 | ||
assert range_widget.spinbox_end.value() == 62.3 | ||
|
||
|
||
if __name__ == '__main__': | ||
pytest.main(['-x', __file__, '-vv', '-rw']) |