-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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 #4596 from ales-erjavec/gradient-selection-widget
[ENH] Gradient selection/parameters widget
- Loading branch information
Showing
7 changed files
with
279 additions
and
77 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
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,151 @@ | ||
from typing import Any, Tuple | ||
|
||
from AnyQt.QtCore import Qt, QSize, QAbstractItemModel, Property | ||
from AnyQt.QtWidgets import ( | ||
QWidget, QSlider, QFormLayout, QComboBox, QStyle | ||
) | ||
from AnyQt.QtCore import Signal | ||
|
||
from Orange.widgets.utils import itemmodels | ||
|
||
|
||
class ColorGradientSelection(QWidget): | ||
activated = Signal(int) | ||
|
||
currentIndexChanged = Signal(int) | ||
thresholdsChanged = Signal(float, float) | ||
|
||
def __init__(self, *args, thresholds=(0.0, 1.0), **kwargs): | ||
super().__init__(*args, **kwargs) | ||
|
||
low = round(clip(thresholds[0], 0., 1.), 2) | ||
high = round(clip(thresholds[1], 0., 1.), 2) | ||
high = max(low, high) | ||
self.__threshold_low, self.__threshold_high = low, high | ||
form = QFormLayout( | ||
formAlignment=Qt.AlignLeft, | ||
labelAlignment=Qt.AlignLeft, | ||
fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow | ||
) | ||
form.setContentsMargins(0, 0, 0, 0) | ||
self.gradient_cb = QComboBox( | ||
None, objectName="gradient-combo-box", | ||
) | ||
self.gradient_cb.setAttribute(Qt.WA_LayoutUsesWidgetRect) | ||
icsize = self.style().pixelMetric( | ||
QStyle.PM_SmallIconSize, None, self.gradient_cb | ||
) | ||
self.gradient_cb.setIconSize(QSize(64, icsize)) | ||
model = itemmodels.ContinuousPalettesModel() | ||
model.setParent(self) | ||
|
||
self.gradient_cb.setModel(model) | ||
self.gradient_cb.activated[int].connect(self.activated) | ||
self.gradient_cb.currentIndexChanged.connect(self.currentIndexChanged) | ||
|
||
slider_low = QSlider( | ||
objectName="threshold-low-slider", minimum=0, maximum=100, | ||
value=int(low * 100), orientation=Qt.Horizontal, | ||
tickPosition=QSlider.TicksBelow, pageStep=10, | ||
toolTip=self.tr("Low gradient threshold"), | ||
whatsThis=self.tr("Applying a low threshold will squeeze the " | ||
"gradient from the lower end") | ||
) | ||
slider_high = QSlider( | ||
objectName="threshold-low-slider", minimum=0, maximum=100, | ||
value=int(high * 100), orientation=Qt.Horizontal, | ||
tickPosition=QSlider.TicksAbove, pageStep=10, | ||
toolTip=self.tr("High gradient threshold"), | ||
whatsThis=self.tr("Applying a high threshold will squeeze the " | ||
"gradient from the higher end") | ||
) | ||
form.setWidget(0, QFormLayout.SpanningRole, self.gradient_cb) | ||
form.addRow(self.tr("Low:"), slider_low) | ||
form.addRow(self.tr("High:"), slider_high) | ||
self.slider_low = slider_low | ||
self.slider_high = slider_high | ||
self.slider_low.valueChanged.connect(self.__on_slider_low_moved) | ||
self.slider_high.valueChanged.connect(self.__on_slider_high_moved) | ||
self.setLayout(form) | ||
|
||
def setModel(self, model: QAbstractItemModel) -> None: | ||
self.gradient_cb.setModel(model) | ||
|
||
def model(self) -> QAbstractItemModel: | ||
return self.gradient_cb.model() | ||
|
||
def findData(self, data: Any, role: Qt.ItemDataRole) -> int: | ||
return self.gradient_cb.findData(data, role) | ||
|
||
def setCurrentIndex(self, index: int) -> None: | ||
self.gradient_cb.setCurrentIndex(index) | ||
|
||
def currentIndex(self) -> int: | ||
return self.gradient_cb.currentIndex() | ||
|
||
currentIndex_ = Property( | ||
int, currentIndex, setCurrentIndex, notify=currentIndexChanged) | ||
|
||
def currentData(self, role=Qt.UserRole) -> Any: | ||
return self.gradient_cb.currentData(role) | ||
|
||
def thresholds(self) -> Tuple[float, float]: | ||
return self.__threshold_low, self.__threshold_high | ||
|
||
thresholds_ = Property(object, thresholds, notify=thresholdsChanged) | ||
|
||
def thresholdLow(self) -> float: | ||
return self.__threshold_low | ||
|
||
def setThresholdLow(self, low: float) -> None: | ||
self.setThresholds(low, max(self.__threshold_high, low)) | ||
|
||
thresholdLow_ = Property( | ||
float, thresholdLow, setThresholdLow, notify=thresholdsChanged) | ||
|
||
def thresholdHigh(self) -> float: | ||
return self.__threshold_high | ||
|
||
def setThresholdHigh(self, high: float) -> None: | ||
self.setThresholds(min(self.__threshold_low, high), high) | ||
|
||
thresholdHigh_ = Property( | ||
float, thresholdLow, setThresholdLow, notify=thresholdsChanged) | ||
|
||
def __on_slider_low_moved(self, value: int) -> None: | ||
high = self.slider_high | ||
old = self.__threshold_low, self.__threshold_high | ||
self.__threshold_low = value / 100. | ||
if value >= high.value(): | ||
self.__threshold_high = value / 100. | ||
high.setSliderPosition(value) | ||
new = self.__threshold_low, self.__threshold_high | ||
if new != old: | ||
self.thresholdsChanged.emit(*new) | ||
|
||
def __on_slider_high_moved(self, value: int) -> None: | ||
low = self.slider_low | ||
old = self.__threshold_low, self.__threshold_high | ||
self.__threshold_high = value / 100. | ||
if low.value() >= value: | ||
self.__threshold_low = value / 100 | ||
low.setSliderPosition(value) | ||
new = self.__threshold_low, self.__threshold_high | ||
if new != old: | ||
self.thresholdsChanged.emit(*new) | ||
|
||
def setThresholds(self, low: float, high: float) -> None: | ||
low = round(clip(low, 0., 1.), 2) | ||
high = round(clip(high, 0., 1.), 2) | ||
if low > high: | ||
high = low | ||
if self.__threshold_low != low or self.__threshold_high != high: | ||
self.__threshold_high = high | ||
self.__threshold_low = low | ||
self.slider_low.setSliderPosition(low * 100) | ||
self.slider_high.setSliderPosition(high * 100) | ||
self.thresholdsChanged.emit(high, low) | ||
|
||
|
||
def clip(a, amin, amax): | ||
return min(max(a, amin), amax) |
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
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,70 @@ | ||
import numpy as np | ||
|
||
from AnyQt.QtTest import QSignalSpy | ||
from AnyQt.QtCore import Qt, QStringListModel | ||
|
||
from Orange.widgets.utils.colorgradientselection import ColorGradientSelection | ||
from Orange.widgets.tests.base import GuiTest | ||
|
||
class TestColorGradientSelection(GuiTest): | ||
def test_constructor(self): | ||
w = ColorGradientSelection(thresholds=(0.1, 0.9)) | ||
self.assertEqual(w.thresholds(), (0.1, 0.9)) | ||
|
||
w = ColorGradientSelection(thresholds=(-0.1, 1.1)) | ||
self.assertEqual(w.thresholds(), (0.0, 1.0)) | ||
|
||
w = ColorGradientSelection(thresholds=(1.0, 0.0)) | ||
self.assertEqual(w.thresholds(), (1.0, 1.0)) | ||
|
||
def test_setModel(self): | ||
w = ColorGradientSelection() | ||
model = QStringListModel(["A", "B"]) | ||
w.setModel(model) | ||
self.assertIs(w.model(), model) | ||
self.assertEqual(w.findData("B", Qt.DisplayRole), 1) | ||
current = QSignalSpy(w.currentIndexChanged) | ||
w.setCurrentIndex(1) | ||
self.assertEqual(w.currentIndex(), 1) | ||
self.assertSequenceEqual(list(current), [[1]]) | ||
|
||
def test_thresholds(self): | ||
w = ColorGradientSelection() | ||
w.setThresholds(0.2, 0.8) | ||
self.assertEqual(w.thresholds(), (0.2, 0.8)) | ||
w.setThresholds(0.5, 0.5) | ||
self.assertEqual(w.thresholds(), (0.5, 0.5)) | ||
w.setThresholds(0.5, np.nextafter(0.5, 0)) | ||
self.assertEqual(w.thresholds(), (0.5, 0.5)) | ||
w.setThresholds(-1, 2) | ||
self.assertEqual(w.thresholds(), (0., 1.)) | ||
w.setThresholds(0.1, 0.0) | ||
self.assertEqual(w.thresholds(), (0.1, 0.1)) | ||
w.setThresholdLow(0.2) | ||
self.assertEqual(w.thresholds(), (0.2, 0.2)) | ||
self.assertEqual(w.thresholdLow(), 0.2) | ||
w.setThresholdHigh(0.1) | ||
self.assertEqual(w.thresholdHigh(), 0.1) | ||
self.assertEqual(w.thresholds(), (0.1, 0.1)) | ||
|
||
def test_slider_move(self): | ||
w = ColorGradientSelection() | ||
w.adjustSize() | ||
w.setThresholds(0.5, 0.5) | ||
changed = QSignalSpy(w.thresholdsChanged) | ||
sl, sh = w.slider_low, w.slider_high | ||
sl.triggerAction(sl.SliderToMinimum) | ||
self.assertEqual(len(changed), 1) | ||
low, high = changed[-1] | ||
self.assertLessEqual(low, high) | ||
self.assertEqual(low, 0.0) | ||
sl.triggerAction(sl.SliderToMaximum) | ||
self.assertEqual(len(changed), 2) | ||
low, high = changed[-1] | ||
self.assertLessEqual(low, high) | ||
self.assertEqual(low, 1.0) | ||
sh.triggerAction(sl.SliderToMinimum) | ||
self.assertEqual(len(changed), 3) | ||
low, high = changed[-1] | ||
self.assertLessEqual(low, high) | ||
self.assertEqual(high, 0.0) |
Oops, something went wrong.