Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a27b388f3e | ||
|
21523dee82 | ||
|
9471796fe5 | ||
|
a6b0518be5 | ||
|
592f0d75ba | ||
|
2897a18851 | ||
|
59c5dec044 | ||
|
1340bfa371 | ||
|
7d0ab56d54 | ||
|
4edcdf4941 | ||
|
b651e2b757 | ||
|
7ad87f9dc6 | ||
|
7d323240be | ||
|
e56d96fa5a | ||
|
69203f878f | ||
|
e8594d8b40 | ||
|
01f496bc18 | ||
|
75b29bc600 |
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
Screenshots and GIFS are much appreciated when reporting visual bugs.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS with version [e.g macOS 10.15.7]
|
||||
- Qt Backend [e.g PyQt5, PySide2]
|
||||
- Python version
|
@@ -9,15 +9,15 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.12.0
|
||||
rev: v2.13.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
rev: 21.4b0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 3.9.0
|
||||
rev: 3.9.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
pass_filenames: true
|
||||
|
140
README.md
@@ -55,7 +55,7 @@ range_slider = QRangeSlider()
|
||||
As `QRangeSlider` inherits from `QtWidgets.QSlider`, you can use all of the
|
||||
same methods available in the [QSlider API](https://doc.qt.io/qt-5/qslider.html). The major difference is that `value` and `sliderPosition` are reimplemented as `tuples` of `int` (where the length of the tuple is equal to the number of handles in the slider.)
|
||||
|
||||
### value: Tuple[int, ...]
|
||||
### `value: Tuple[int, ...]`
|
||||
|
||||
This property holds the current value of all handles in the slider.
|
||||
|
||||
@@ -80,7 +80,7 @@ range_slider.setValue(val: Sequence[int]) -> None
|
||||
valueChanged(Tuple[int, ...])
|
||||
```
|
||||
|
||||
### sliderPosition: Tuple[int, ...]
|
||||
### `sliderPosition: Tuple[int, ...]`
|
||||
|
||||
This property holds the current slider positions. It is a `tuple` with length equal to the number of handles.
|
||||
|
||||
@@ -102,9 +102,18 @@ range_slider.setSliderPosition(val: Sequence[int]) -> None
|
||||
sliderMoved(Tuple[int, ...])
|
||||
```
|
||||
|
||||
### Additional properties
|
||||
|
||||
These options are in addition to the Qt QSlider API, and control the behavior of the bar between handles.
|
||||
|
||||
| getter | setter | type | default | description |
|
||||
| -------------------- | ------------------------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `barIsVisible` | `setBarIsVisible` <br>`hideBar` / `showBar` | `bool` | `True` | <small>Whether the bar between handles is visible.</small> |
|
||||
| `barMovesAllHandles` | `setBarMovesAllHandles` | `bool` | `True` | <small>Whether clicking on the bar moves all handles or just the nearest</small> |
|
||||
| `barIsRigid` | `setBarIsRigid` | `bool` | `True` | <small>Whether bar length is constant or "elastic" when dragging the bar beyond min/max.</small> |
|
||||
------
|
||||
|
||||
## Example
|
||||
## Examples
|
||||
|
||||
These screenshots show `QRangeSlider` (multiple handles) next to the native `QSlider`
|
||||
(single handle). With no styles applied, `QRangeSlider` will match the native OS
|
||||
@@ -112,7 +121,9 @@ style of `QSlider` – with or without tick marks. When styles have been applie
|
||||
using [Qt Style Sheets](https://doc.qt.io/qt-5/stylesheet-reference.html), then
|
||||
`QRangeSlider` will inherit any styles applied to `QSlider` (since it inherits
|
||||
from QSlider). If you'd like to style `QRangeSlider` differently than `QSlider`,
|
||||
then you can also target it directly in your style sheet.
|
||||
then you can also target it directly in your style sheet. The one "special"
|
||||
property for QRangeSlider is `qproperty-barColor`, which sets the color of the
|
||||
bar between the handles.
|
||||
|
||||
> The code for these example widgets is [here](examples/demo_widget.py)
|
||||
|
||||
@@ -121,30 +132,44 @@ then you can also target it directly in your style sheet.
|
||||
<summary><em>See style sheet used for this example</em></summary>
|
||||
|
||||
```css
|
||||
/* Because QRangeSlider inherits QSlider, it will also inherit styles */
|
||||
/*
|
||||
Because QRangeSlider inherits from QSlider, it will also inherit styles
|
||||
*/
|
||||
QSlider {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
QSlider::groove:horizontal {
|
||||
border: 0px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #FDE282, stop:1 #EB9A5D);
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
border: 0px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #777, stop:1 #aaa);
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal {
|
||||
background: #271848;
|
||||
border: 1px solid #583856;
|
||||
width: 18px;
|
||||
margin: -2px 0;
|
||||
border-radius: 3px;
|
||||
QSlider::handle {
|
||||
background: qradialgradient(cx:0, cy:0, radius: 1.2, fx:0.5,
|
||||
fy:0.5, stop:0 #eef, stop:1 #000);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QSlider::handle:hover {
|
||||
background-color: #2F4F4F;
|
||||
}
|
||||
|
||||
/* "QSlider::sub-page" will style the "bar" area between the QRangeSlider handles */
|
||||
/*
|
||||
"QSlider::sub-page" is the one exception ...
|
||||
(it styles the area to the left of the QSlider handle)
|
||||
*/
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #AF5A50;
|
||||
border-radius: 2px;
|
||||
background: #447;
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
for QRangeSlider: use "qproperty-barColor". "sub-page" will not work.
|
||||
*/
|
||||
QRangeSlider {
|
||||
qproperty-barColor: #447;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -153,18 +178,81 @@ QSlider::sub-page:horizontal {
|
||||
### macOS
|
||||
|
||||
##### Catalina
|
||||

|
||||

|
||||
|
||||
##### Big Sur
|
||||

|
||||

|
||||
|
||||
### Windows
|
||||
|
||||

|
||||

|
||||
|
||||
### Linux
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Labeled Sliders
|
||||
|
||||
This package also includes two "labeled" slider variants. One for `QRangeSlider`, and one for the native `QSlider`:
|
||||
|
||||
### `QLabeledRangeSlider`
|
||||
|
||||

|
||||
|
||||
```python
|
||||
from qtrangeslider import QLabeledRangeSlider
|
||||
```
|
||||
|
||||
This has the same API as `QRangeSlider` with the following additional options:
|
||||
|
||||
#### `handleLabelPosition`/`setHandleLabelPosition`
|
||||
|
||||
Where/whether labels are shown adjacent to slider handles.
|
||||
|
||||
**type:** `QLabeledRangeSlider.LabelPosition`
|
||||
|
||||
**default:** `LabelPosition.LabelsAbove`
|
||||
|
||||
*options:*
|
||||
|
||||
- `LabelPosition.NoLabel` (no labels shown adjacent to handles)
|
||||
- `LabelPosition.LabelsAbove`
|
||||
- `LabelPosition.LabelsBelow`
|
||||
- `LabelPosition.LabelsRight` (alias for `LabelPosition.LabelsAbove`)
|
||||
- `LabelPosition.LabelsLeft` (alias for `LabelPosition.LabelsBelow`)
|
||||
|
||||
|
||||
#### `edgeLabelMode`/`setEdgeLabelMode`
|
||||
|
||||
**type:** `QLabeledRangeSlider.EdgeLabelMode`
|
||||
|
||||
**default:** `EdgeLabelMode.LabelsAbove`
|
||||
|
||||
*options:*
|
||||
|
||||
- `EdgeLabelMode.NoLabel`: no labels shown at slider extremes
|
||||
- `EdgeLabelMode.LabelIsRange`: edge labels shown the min/max values
|
||||
- `EdgeLabelMode.LabelIsValue`: edge labels shown the slider range
|
||||
|
||||
|
||||
#### fine tuning position of labels:
|
||||
|
||||
If you find that you need to fine tune the position of the handle labels:
|
||||
|
||||
- `QLabeledRangeSlider.label_shift_x`: adjust horizontal label position
|
||||
- `QLabeledRangeSlider.label_shift_y`: adjust vertical label position
|
||||
|
||||
### `QLabeledSlider`
|
||||
|
||||
|
||||

|
||||
|
||||
```python
|
||||
from qtrangeslider import QLabeledSlider
|
||||
```
|
||||
|
||||
(no additional options at this point)
|
||||
|
||||
## Issues
|
||||
|
||||
|
@@ -1,9 +1,10 @@
|
||||
from qtrangeslider import QRangeSlider
|
||||
from qtrangeslider.qtcompat.QtCore import Qt
|
||||
from qtrangeslider.qtcompat.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QRangeSlider()
|
||||
slider = QRangeSlider(Qt.Horizontal)
|
||||
|
||||
slider.setValue((20, 80))
|
||||
slider.show()
|
||||
|
@@ -3,31 +3,34 @@ from qtrangeslider.qtcompat import QtCore
|
||||
from qtrangeslider.qtcompat import QtWidgets as QtW
|
||||
|
||||
QSS = """
|
||||
QSlider {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
QSlider::groove:horizontal {
|
||||
border: 0px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #FDE282, stop:1 #EB9A5D);
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
border: 0px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #888, stop:1 #ddd);
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal {
|
||||
background: #271848;
|
||||
border: 1px solid #583856;
|
||||
width: 18px;
|
||||
margin: -2px 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
QSlider::handle:hover {
|
||||
background-color: #2F4F4F;
|
||||
QSlider::handle {
|
||||
background: qradialgradient(cx:0, cy:0, radius: 1.2, fx:0.35,
|
||||
fy:0.3, stop:0 #eef, stop:1 #002);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #AF5A50;
|
||||
border-radius: 2px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #227, stop:1 #77a);
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
QRangeSlider {
|
||||
qproperty-barColor: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #227, stop:1 #77a);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
|
17
examples/labeled.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from qtrangeslider._labeled import QLabeledRangeSlider, QLabeledSlider
|
||||
from qtrangeslider.qtcompat.QtCore import Qt
|
||||
from qtrangeslider.qtcompat.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
w = QWidget()
|
||||
sld = QLabeledRangeSlider()
|
||||
|
||||
sld.setRange(0, 500)
|
||||
sld.setValue((100, 400))
|
||||
w.setLayout(QVBoxLayout())
|
||||
w.layout().addWidget(sld)
|
||||
w.layout().addWidget(QLabeledSlider(Qt.Horizontal))
|
||||
w.show()
|
||||
w.resize(500, 150)
|
||||
app.exec_()
|
Before Width: | Height: | Size: 38 KiB |
BIN
images/demo_darwin10.png
Normal file
After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 9.0 KiB |
BIN
images/labeled_qslider.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
images/labeled_range.png
Normal file
After Width: | Height: | Size: 12 KiB |
3
pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# pyproject.toml
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"]
|
@@ -3,6 +3,7 @@ try:
|
||||
except ImportError:
|
||||
__version__ = "unknown"
|
||||
|
||||
from ._labeled import QLabeledRangeSlider, QLabeledSlider
|
||||
from ._qrangeslider import QRangeSlider
|
||||
|
||||
__all__ = ["QRangeSlider"]
|
||||
__all__ = ["QRangeSlider", "QLabeledRangeSlider", "QLabeledSlider"]
|
||||
|
362
qtrangeslider/_labeled.py
Normal file
@@ -0,0 +1,362 @@
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
|
||||
from ._qrangeslider import QRangeSlider
|
||||
from .qtcompat.QtCore import QPoint, QSize, Qt, Signal
|
||||
from .qtcompat.QtGui import QFontMetrics
|
||||
from .qtcompat.QtWidgets import (
|
||||
QAbstractSlider,
|
||||
QApplication,
|
||||
QHBoxLayout,
|
||||
QSlider,
|
||||
QSpinBox,
|
||||
QStyle,
|
||||
QStyleOptionSpinBox,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class LabelPosition(IntEnum):
|
||||
NoLabel = 0
|
||||
LabelsAbove = 1
|
||||
LabelsBelow = 2
|
||||
LabelsRight = 1
|
||||
LabelsLeft = 2
|
||||
|
||||
|
||||
class EdgeLabelMode(IntEnum):
|
||||
NoLabel = 0
|
||||
LabelIsRange = 1
|
||||
LabelIsValue = 2
|
||||
|
||||
|
||||
class QLabeledSlider(QAbstractSlider):
|
||||
def __init__(self, *args) -> None:
|
||||
parent = None
|
||||
orientation = Qt.Horizontal
|
||||
if len(args) == 2:
|
||||
orientation, parent = args
|
||||
elif args:
|
||||
if isinstance(args[0], QWidget):
|
||||
parent = args[0]
|
||||
else:
|
||||
orientation = args[0]
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self._slider = QSlider()
|
||||
self._slider.valueChanged.connect(self.valueChanged.emit)
|
||||
self._label = SliderLabel(self._slider, connect=self.setValue)
|
||||
|
||||
self.valueChanged.connect(self._label.setValue)
|
||||
self.valueChanged.connect(self._slider.setValue)
|
||||
self.rangeChanged.connect(self._slider.setRange)
|
||||
|
||||
self._slider.valueChanged.connect(self.setValue)
|
||||
self.setOrientation(orientation)
|
||||
|
||||
def setOrientation(self, orientation):
|
||||
"""Set orientation, value will be 'horizontal' or 'vertical'."""
|
||||
self._slider.setOrientation(orientation)
|
||||
if orientation == Qt.Vertical:
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self._slider, alignment=Qt.AlignHCenter)
|
||||
layout.addWidget(self._label, alignment=Qt.AlignHCenter)
|
||||
self._label.setAlignment(Qt.AlignCenter)
|
||||
layout.setSpacing(1)
|
||||
else:
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(self._slider)
|
||||
layout.addWidget(self._label)
|
||||
self._label.setAlignment(Qt.AlignRight)
|
||||
layout.setSpacing(10)
|
||||
|
||||
old_layout = self.layout()
|
||||
if old_layout is not None:
|
||||
QWidget().setLayout(old_layout)
|
||||
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
class QLabeledRangeSlider(QAbstractSlider):
|
||||
valueChanged = Signal(tuple)
|
||||
LabelPosition = LabelPosition
|
||||
EdgeLabelMode = EdgeLabelMode
|
||||
|
||||
def __init__(self, *args) -> None:
|
||||
parent = None
|
||||
orientation = Qt.Horizontal
|
||||
if len(args) == 2:
|
||||
orientation, parent = args
|
||||
elif args:
|
||||
if isinstance(args[0], QWidget):
|
||||
parent = args[0]
|
||||
else:
|
||||
orientation = args[0]
|
||||
|
||||
super().__init__(parent)
|
||||
self.setAttribute(Qt.WA_ShowWithoutActivating)
|
||||
self._handle_labels = []
|
||||
self._handle_label_position: LabelPosition = LabelPosition.LabelsAbove
|
||||
|
||||
# for fine tuning label position
|
||||
self.label_shift_x = 0
|
||||
self.label_shift_y = 0
|
||||
|
||||
self._slider = QRangeSlider()
|
||||
self._slider.valueChanged.connect(self.valueChanged.emit)
|
||||
|
||||
self._min_label = SliderLabel(
|
||||
self._slider, alignment=Qt.AlignLeft, connect=self._min_label_edited
|
||||
)
|
||||
self._max_label = SliderLabel(
|
||||
self._slider, alignment=Qt.AlignRight, connect=self._max_label_edited
|
||||
)
|
||||
self.setEdgeLabelMode(EdgeLabelMode.LabelIsRange)
|
||||
|
||||
self._slider.valueChanged.connect(self._on_value_changed)
|
||||
self.rangeChanged.connect(self._on_range_changed)
|
||||
|
||||
self._on_value_changed(self._slider.value())
|
||||
self._on_range_changed(self._slider.minimum(), self._slider.maximum())
|
||||
self.setOrientation(orientation)
|
||||
|
||||
def handleLabelPosition(self) -> LabelPosition:
|
||||
return self._handle_label_position
|
||||
|
||||
def setHandleLabelPosition(self, opt: LabelPosition) -> LabelPosition:
|
||||
self._handle_label_position = opt
|
||||
for lbl in self._handle_labels:
|
||||
if not opt:
|
||||
lbl.hide()
|
||||
else:
|
||||
lbl.show()
|
||||
self.setOrientation(self.orientation())
|
||||
|
||||
def edgeLabelMode(self) -> EdgeLabelMode:
|
||||
return self._edge_label_mode
|
||||
|
||||
def setEdgeLabelMode(self, opt: EdgeLabelMode):
|
||||
self._edge_label_mode = opt
|
||||
if not self._edge_label_mode:
|
||||
self._min_label.hide()
|
||||
self._max_label.hide()
|
||||
else:
|
||||
if self.isVisible():
|
||||
self._min_label.show()
|
||||
self._max_label.show()
|
||||
self._min_label.setMode(opt)
|
||||
self._max_label.setMode(opt)
|
||||
if opt == EdgeLabelMode.LabelIsValue:
|
||||
v0, *_, v1 = self._slider.value()
|
||||
self._min_label.setValue(v0)
|
||||
self._max_label.setValue(v1)
|
||||
elif opt == EdgeLabelMode.LabelIsRange:
|
||||
self._min_label.setValue(self._slider.minimum())
|
||||
self._max_label.setValue(self._slider.maximum())
|
||||
QApplication.processEvents()
|
||||
self._reposition_labels()
|
||||
|
||||
def _reposition_labels(self):
|
||||
if not self._handle_labels:
|
||||
return
|
||||
|
||||
horizontal = self.orientation() == Qt.Horizontal
|
||||
labels_above = self._handle_label_position == LabelPosition.LabelsAbove
|
||||
|
||||
for label, rect in zip(self._handle_labels, self._slider._handleRects()):
|
||||
dx = -label.width() / 2
|
||||
dy = -label.height() / 2
|
||||
if labels_above:
|
||||
if horizontal:
|
||||
dy *= 3
|
||||
else:
|
||||
dx *= -1
|
||||
else:
|
||||
if horizontal:
|
||||
dy *= -1
|
||||
else:
|
||||
dx *= 3
|
||||
pos = self._slider.mapToParent(rect.center())
|
||||
pos += QPoint(dx + self.label_shift_x, dy + self.label_shift_y)
|
||||
label.move(pos)
|
||||
label.clearFocus()
|
||||
|
||||
def _min_label_edited(self, val):
|
||||
if self._edge_label_mode == EdgeLabelMode.LabelIsRange:
|
||||
self.setMinimum(val)
|
||||
else:
|
||||
v = list(self._slider.value())
|
||||
v[0] = val
|
||||
self.setValue(v)
|
||||
self._reposition_labels()
|
||||
|
||||
def _max_label_edited(self, val):
|
||||
if self._edge_label_mode == EdgeLabelMode.LabelIsRange:
|
||||
self.setMaximum(val)
|
||||
else:
|
||||
v = list(self._slider.value())
|
||||
v[-1] = val
|
||||
self.setValue(v)
|
||||
self._reposition_labels()
|
||||
|
||||
def _on_value_changed(self, v):
|
||||
if self._edge_label_mode == EdgeLabelMode.LabelIsValue:
|
||||
self._min_label.setValue(v[0])
|
||||
self._max_label.setValue(v[-1])
|
||||
|
||||
if len(v) != len(self._handle_labels):
|
||||
for lbl in self._handle_labels:
|
||||
lbl.setParent(None)
|
||||
lbl.deleteLater()
|
||||
self._handle_labels.clear()
|
||||
for n, val in enumerate(self._slider.value()):
|
||||
_cb = partial(self._slider._setSliderPositionAt, n)
|
||||
s = SliderLabel(self._slider, parent=self, connect=_cb)
|
||||
s.setValue(val)
|
||||
self._handle_labels.append(s)
|
||||
else:
|
||||
for val, label in zip(v, self._handle_labels):
|
||||
label.setValue(val)
|
||||
self._reposition_labels()
|
||||
|
||||
def _on_range_changed(self, min, max):
|
||||
self._slider.setRange(min, max)
|
||||
for lbl in self._handle_labels:
|
||||
lbl.setRange(min, max)
|
||||
if self._edge_label_mode == EdgeLabelMode.LabelIsRange:
|
||||
self._min_label.setValue(min)
|
||||
self._max_label.setValue(max)
|
||||
self._reposition_labels()
|
||||
|
||||
def value(self):
|
||||
return self._slider.value()
|
||||
|
||||
def setValue(self, v: int) -> None:
|
||||
self._slider.setValue(v)
|
||||
self.sliderChange(QSlider.SliderValueChange)
|
||||
|
||||
def setOrientation(self, orientation):
|
||||
"""Set orientation, value will be 'horizontal' or 'vertical'."""
|
||||
|
||||
self._slider.setOrientation(orientation)
|
||||
if orientation == Qt.Vertical:
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(1)
|
||||
layout.addWidget(self._max_label)
|
||||
layout.addWidget(self._slider)
|
||||
layout.addWidget(self._min_label)
|
||||
# TODO: set margins based on label width
|
||||
if self._handle_label_position == LabelPosition.LabelsLeft:
|
||||
marg = (30, 0, 0, 0)
|
||||
elif self._handle_label_position == LabelPosition.NoLabel:
|
||||
marg = (0, 0, 0, 0)
|
||||
else:
|
||||
marg = (0, 0, 20, 0)
|
||||
layout.setAlignment(Qt.AlignCenter)
|
||||
else:
|
||||
layout = QHBoxLayout()
|
||||
layout.setSpacing(7)
|
||||
if self._handle_label_position == LabelPosition.LabelsBelow:
|
||||
marg = (0, 0, 0, 25)
|
||||
elif self._handle_label_position == LabelPosition.NoLabel:
|
||||
marg = (0, 0, 0, 0)
|
||||
else:
|
||||
marg = (0, 25, 0, 0)
|
||||
layout.addWidget(self._min_label)
|
||||
layout.addWidget(self._slider)
|
||||
layout.addWidget(self._max_label)
|
||||
|
||||
# remove old layout
|
||||
old_layout = self.layout()
|
||||
if old_layout is not None:
|
||||
QWidget().setLayout(old_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
layout.setContentsMargins(*marg)
|
||||
super().setOrientation(orientation)
|
||||
QApplication.processEvents()
|
||||
self._reposition_labels()
|
||||
|
||||
def resizeEvent(self, a0) -> None:
|
||||
super().resizeEvent(a0)
|
||||
self._reposition_labels()
|
||||
|
||||
|
||||
class SliderLabel(QSpinBox):
|
||||
def __init__(
|
||||
self, slider: QSlider, parent=None, alignment=Qt.AlignCenter, connect=None
|
||||
) -> None:
|
||||
super().__init__(parent=parent)
|
||||
self._slider = slider
|
||||
self.setFocusPolicy(Qt.ClickFocus)
|
||||
self.setMode(EdgeLabelMode.LabelIsValue)
|
||||
|
||||
self.setRange(slider.minimum(), slider.maximum())
|
||||
slider.rangeChanged.connect(self._update_size)
|
||||
self.setAlignment(alignment)
|
||||
self.setButtonSymbols(QSpinBox.NoButtons)
|
||||
self.setStyleSheet("background:transparent; border: 0;")
|
||||
if connect is not None:
|
||||
self.editingFinished.connect(lambda: connect(self.value()))
|
||||
self.editingFinished.connect(self.clearFocus)
|
||||
self._update_size()
|
||||
|
||||
def _update_size(self):
|
||||
# fontmetrics to measure the width of text
|
||||
fm = QFontMetrics(self.font())
|
||||
h = self.sizeHint().height()
|
||||
fixed_content = self.prefix() + self.suffix() + " "
|
||||
|
||||
if self._mode == EdgeLabelMode.LabelIsValue:
|
||||
# determine width based on min/max/specialValue
|
||||
s = self.textFromValue(self.minimum())[:18] + fixed_content
|
||||
w = max(0, fm.horizontalAdvance(s))
|
||||
s = self.textFromValue(self.maximum())[:18] + fixed_content
|
||||
w = max(w, fm.horizontalAdvance(s))
|
||||
if self.specialValueText():
|
||||
w = max(w, fm.horizontalAdvance(self.specialValueText()))
|
||||
else:
|
||||
s = self.textFromValue(self.value())
|
||||
w = max(0, fm.horizontalAdvance(s)) + 3
|
||||
|
||||
w += 3 # cursor blinking space
|
||||
# get the final size hint
|
||||
opt = QStyleOptionSpinBox()
|
||||
self.initStyleOption(opt)
|
||||
size = self.style().sizeFromContents(QStyle.CT_SpinBox, opt, QSize(w, h), self)
|
||||
self.setFixedSize(size)
|
||||
|
||||
def setValue(self, val):
|
||||
super().setValue(val)
|
||||
if self._mode == EdgeLabelMode.LabelIsRange:
|
||||
self._update_size()
|
||||
|
||||
def setMaximum(self, max: int) -> None:
|
||||
super().setMaximum(max)
|
||||
if self._mode == EdgeLabelMode.LabelIsValue:
|
||||
self._update_size()
|
||||
|
||||
def setMinimum(self, min: int) -> None:
|
||||
super().setMinimum(min)
|
||||
if self._mode == EdgeLabelMode.LabelIsValue:
|
||||
self._update_size()
|
||||
|
||||
def setMode(self, opt: EdgeLabelMode):
|
||||
# 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)
|
||||
try:
|
||||
self._slider.rangeChanged.disconnect(self.setRange)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self.setMinimum(self._slider.minimum())
|
||||
self.setMaximum(self._slider.maximum())
|
||||
self._slider.rangeChanged.connect(self.setRange)
|
||||
self._update_size()
|
@@ -4,7 +4,16 @@ from typing import List, Sequence, Tuple
|
||||
|
||||
from ._style import RangeSliderStyle, update_styles_from_stylesheet
|
||||
from .qtcompat import QtGui
|
||||
from .qtcompat.QtCore import QEvent, QPoint, QPointF, QRect, QRectF, Qt, Signal
|
||||
from .qtcompat.QtCore import (
|
||||
Property,
|
||||
QEvent,
|
||||
QPoint,
|
||||
QPointF,
|
||||
QRect,
|
||||
QRectF,
|
||||
Qt,
|
||||
Signal,
|
||||
)
|
||||
from .qtcompat.QtWidgets import (
|
||||
QApplication,
|
||||
QSlider,
|
||||
@@ -35,17 +44,16 @@ class QRangeSlider(QSlider):
|
||||
sliderMoved = Signal(tuple)
|
||||
|
||||
_NULL_CTRL = ("None", -1)
|
||||
_DEFAULT_VALUE = (20, 80)
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
# list of values
|
||||
self._value: List[int] = self._DEFAULT_VALUE
|
||||
self._value: List[int] = [20, 80]
|
||||
|
||||
# list of current positions of each handle. same length as _value
|
||||
# If tracking is enabled (the default) this will be identical to _value
|
||||
self._position: List[int] = self._DEFAULT_VALUE
|
||||
self._position: List[int] = [20, 80]
|
||||
self._pressedControl: ControlType = self._NULL_CTRL
|
||||
self._hoverControl: ControlType = self._NULL_CTRL
|
||||
|
||||
@@ -63,9 +71,19 @@ class QRangeSlider(QSlider):
|
||||
|
||||
# color
|
||||
self._style = RangeSliderStyle()
|
||||
self.setStyleSheet("")
|
||||
update_styles_from_stylesheet(self)
|
||||
|
||||
# ############### Public API #######################
|
||||
|
||||
def setStyleSheet(self, styleSheet: str) -> None:
|
||||
# sub-page styles render on top of the lower sliders and don't work here.
|
||||
override = f"""
|
||||
\n{type(self).__name__}::sub-page:horizontal {{background: none}}
|
||||
\n{type(self).__name__}::sub-page:vertical {{background: none}}
|
||||
"""
|
||||
return super().setStyleSheet(styleSheet + override)
|
||||
|
||||
def value(self) -> Tuple[int, ...]:
|
||||
"""Get current value of the widget as a tuple of integers."""
|
||||
return tuple(self._value)
|
||||
@@ -108,7 +126,8 @@ class QRangeSlider(QSlider):
|
||||
)
|
||||
|
||||
for i, v in enumerate(val):
|
||||
self._setSliderPositionAt(i, v, _update=i == len(val) - 1)
|
||||
self._setSliderPositionAt(i, v, _update=False)
|
||||
self._updateSliderMove()
|
||||
|
||||
def barIsRigid(self) -> bool:
|
||||
"""Whether bar length is constant when dragging the bar.
|
||||
@@ -156,23 +175,36 @@ class QRangeSlider(QSlider):
|
||||
return
|
||||
self._position[index] = pos
|
||||
if _update:
|
||||
if not self.hasTracking():
|
||||
self.update()
|
||||
if self.isSliderDown():
|
||||
self.sliderMoved.emit(tuple(self._position))
|
||||
if self.hasTracking():
|
||||
self.triggerAction(QSlider.SliderMove)
|
||||
self._updateSliderMove()
|
||||
|
||||
def _updateSliderMove(self):
|
||||
if not self.hasTracking():
|
||||
self.update()
|
||||
if self.isSliderDown():
|
||||
self.sliderMoved.emit(tuple(self._position))
|
||||
if self.hasTracking():
|
||||
self.triggerAction(QSlider.SliderMove)
|
||||
|
||||
def _offsetAllPositions(self, offset: int, ref=None) -> None:
|
||||
if ref is None:
|
||||
ref = self._position
|
||||
_new = [i - offset for i in ref]
|
||||
if self._bar_is_rigid:
|
||||
# FIXME: if there is an overflow ... it should still hit the edge.
|
||||
if all(self.minimum() <= i <= self.maximum() for i in _new):
|
||||
self.setSliderPosition(_new)
|
||||
else:
|
||||
self.setSliderPosition(_new)
|
||||
# NOTE: This assumes monotonically increasing slider positions
|
||||
if offset > 0 and ref[-1] + offset > self.maximum():
|
||||
offset = self.maximum() - ref[-1]
|
||||
elif ref[0] + offset < self.minimum():
|
||||
offset = self.minimum() - ref[0]
|
||||
self.setSliderPosition([i + offset for i in ref])
|
||||
|
||||
def _spreadAllPositions(self, shrink=False, gain=1.1, ref=None) -> None:
|
||||
if ref is None:
|
||||
ref = self._position
|
||||
# if self._bar_is_rigid: # TODO
|
||||
|
||||
if shrink:
|
||||
gain = 1 / gain
|
||||
center = abs(ref[-1] + ref[0]) / 2
|
||||
self.setSliderPosition([((i - center) * gain) + center for i in ref])
|
||||
|
||||
def _getStyleOption(self) -> QStyleOptionSlider:
|
||||
opt = QStyleOptionSlider()
|
||||
@@ -181,6 +213,14 @@ class QRangeSlider(QSlider):
|
||||
opt.sliderPosition = 0
|
||||
return opt
|
||||
|
||||
def _getBarColor(self):
|
||||
return self._style.brush(self._getStyleOption())
|
||||
|
||||
def _setBarColor(self, color):
|
||||
self._style.brush_active = color
|
||||
|
||||
barColor = Property(QtGui.QBrush, _getBarColor, _setBarColor)
|
||||
|
||||
def _drawBar(self, painter: QStylePainter, opt: QStyleOptionSlider):
|
||||
|
||||
brush = self._style.brush(opt)
|
||||
@@ -284,8 +324,9 @@ class QRangeSlider(QSlider):
|
||||
self._setSliderPositionAt(self._pressedControl[1], new)
|
||||
elif self._pressedControl[0] == "bar":
|
||||
ev.accept()
|
||||
|
||||
delta = self._clickOffset - self._pixelPosToRangeValue(self._pick(ev.pos()))
|
||||
self._offsetAllPositions(delta, self._sldPosAtPress)
|
||||
self._offsetAllPositions(-delta, self._sldPosAtPress)
|
||||
else:
|
||||
ev.ignore()
|
||||
return
|
||||
@@ -310,8 +351,13 @@ class QRangeSlider(QSlider):
|
||||
super().setRange(min, max)
|
||||
self.setValue(self._value) # re-bound
|
||||
|
||||
def _handleRects(self, opt: QStyleOptionSlider, handle_index: int = None) -> QRect:
|
||||
def _handleRects(
|
||||
self, opt: QStyleOptionSlider = None, handle_index: int = None
|
||||
) -> QRect:
|
||||
"""Return the QRect for all handles."""
|
||||
if opt is None:
|
||||
opt = self._getStyleOption()
|
||||
|
||||
style = self.style().proxy()
|
||||
|
||||
if handle_index is not None: # get specific handle rect
|
||||
@@ -503,7 +549,11 @@ class QRangeSlider(QSlider):
|
||||
|
||||
_prev_value = self.value()
|
||||
|
||||
self._offsetAllPositions(-steps_to_scroll)
|
||||
if modifiers & Qt.AltModifier:
|
||||
|
||||
self._spreadAllPositions(shrink=steps_to_scroll < 0)
|
||||
else:
|
||||
self._offsetAllPositions(steps_to_scroll)
|
||||
self.triggerAction(QSlider.SliderMove)
|
||||
|
||||
if _prev_value == self.value():
|
||||
|
@@ -3,8 +3,10 @@ import re
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from .qtcompat import PYQT_VERSION
|
||||
from .qtcompat.QtCore import Qt
|
||||
from .qtcompat.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
QGradient,
|
||||
QLinearGradient,
|
||||
@@ -33,24 +35,40 @@ class RangeSliderStyle:
|
||||
h_offset: float = None
|
||||
has_stylesheet: bool = False
|
||||
|
||||
def brush(self, opt: QStyleOptionSlider) -> Union[QGradient, QColor]:
|
||||
def brush(self, opt: QStyleOptionSlider) -> QBrush:
|
||||
cg = opt.palette.currentColorGroup()
|
||||
attr = {
|
||||
QPalette.Active: "brush_active", # 0
|
||||
QPalette.Disabled: "brush_disabled", # 1
|
||||
QPalette.Inactive: "brush_inactive", # 2
|
||||
}[cg]
|
||||
val = getattr(self, attr) or getattr(SYSTEM_STYLE, attr)
|
||||
if isinstance(val, str):
|
||||
val = QColor(val)
|
||||
_val = getattr(self, attr)
|
||||
if not _val:
|
||||
if self.has_stylesheet:
|
||||
# if someone set a general style sheet but didn't specify
|
||||
# :active, :inactive, etc... then Qt just uses whatever they
|
||||
# DID specify
|
||||
for i in ("active", "inactive", "disabled"):
|
||||
_val = getattr(self, f"brush_{i}")
|
||||
if _val:
|
||||
break
|
||||
else:
|
||||
_val = getattr(SYSTEM_STYLE, attr)
|
||||
|
||||
if not val:
|
||||
return Qt.NoBrush
|
||||
if _val is None:
|
||||
return QBrush()
|
||||
|
||||
if isinstance(_val, str):
|
||||
val = QColor(_val)
|
||||
if not val.isValid():
|
||||
val = parse_color(_val, default_attr=attr)
|
||||
else:
|
||||
val = _val
|
||||
|
||||
if opt.tickPosition != QSlider.NoTicks:
|
||||
val.setAlphaF(self.tick_bar_alpha or SYSTEM_STYLE.tick_bar_alpha)
|
||||
|
||||
return val
|
||||
return QBrush(val)
|
||||
|
||||
def pen(self, opt: QStyleOptionSlider) -> Union[Qt.PenStyle, QColor]:
|
||||
cg = opt.palette.currentColorGroup()
|
||||
@@ -77,9 +95,9 @@ class RangeSliderStyle:
|
||||
off += self.h_offset or SYSTEM_STYLE.h_offset or 0
|
||||
else:
|
||||
off += self.v_offset or SYSTEM_STYLE.v_offset or 0
|
||||
if tp & QSlider.TicksAbove:
|
||||
if tp == QSlider.TicksAbove:
|
||||
off += self.tick_offset or SYSTEM_STYLE.tick_offset
|
||||
elif tp & QSlider.TicksBelow:
|
||||
elif tp == QSlider.TicksBelow:
|
||||
off -= self.tick_offset or SYSTEM_STYLE.tick_offset
|
||||
return off
|
||||
|
||||
@@ -119,6 +137,9 @@ CATALINA_STYLE = replace(
|
||||
tick_offset=4,
|
||||
)
|
||||
|
||||
if PYQT_VERSION and int(PYQT_VERSION.split(".")[0]) == 6:
|
||||
CATALINA_STYLE = replace(CATALINA_STYLE, tick_offset=2)
|
||||
|
||||
BIG_SUR_STYLE = replace(
|
||||
CATALINA_STYLE,
|
||||
brush_active="#0A81FE",
|
||||
@@ -131,6 +152,9 @@ BIG_SUR_STYLE = replace(
|
||||
tick_bar_alpha=0.2,
|
||||
)
|
||||
|
||||
if PYQT_VERSION and int(PYQT_VERSION.split(".")[0]) == 6:
|
||||
BIG_SUR_STYLE = replace(BIG_SUR_STYLE, tick_offset=-3)
|
||||
|
||||
WINDOWS_STYLE = replace(
|
||||
BASE_STYLE,
|
||||
brush_active="#550179D7",
|
||||
@@ -191,14 +215,29 @@ qradial_pattern = re.compile(
|
||||
re.X,
|
||||
)
|
||||
|
||||
rgba_pattern = re.compile(
|
||||
r"""
|
||||
rgba?\(
|
||||
(?P<r>\d+),\s*
|
||||
(?P<g>\d+),\s*
|
||||
(?P<b>\d+),?\s*(?P<a>\d+)?\)
|
||||
""",
|
||||
re.X,
|
||||
)
|
||||
|
||||
def parse_color(color: str) -> Union[str, QGradient]:
|
||||
|
||||
def parse_color(color: str, default_attr) -> Union[QColor, QGradient]:
|
||||
qc = QColor(color)
|
||||
if qc.isValid():
|
||||
return qc
|
||||
|
||||
match = rgba_pattern.search(color)
|
||||
if match:
|
||||
rgba = [int(x) if x else 255 for x in match.groups()]
|
||||
return QColor(*rgba)
|
||||
|
||||
# try linear gradient:
|
||||
match = qlineargrad_pattern.match(color)
|
||||
match = qlineargrad_pattern.search(color)
|
||||
if match:
|
||||
grad = QLinearGradient(*[float(i) for i in match.groups()[:4]])
|
||||
grad.setColorAt(0, QColor(match.groupdict()["stop0"]))
|
||||
@@ -206,8 +245,7 @@ def parse_color(color: str) -> Union[str, QGradient]:
|
||||
return grad
|
||||
|
||||
# try linear gradient:
|
||||
match = qradial_pattern.match(color)
|
||||
print("match", match.groupdict())
|
||||
match = qradial_pattern.search(color)
|
||||
if match:
|
||||
grad = QRadialGradient(*[float(i) for i in match.groups()[:5]])
|
||||
grad.setColorAt(0, QColor(match.groupdict()["stop0"]))
|
||||
@@ -215,38 +253,19 @@ def parse_color(color: str) -> Union[str, QGradient]:
|
||||
return grad
|
||||
|
||||
# fallback to dark gray
|
||||
return "#333"
|
||||
return QColor(getattr(SYSTEM_STYLE, default_attr))
|
||||
|
||||
|
||||
def update_styles_from_stylesheet(obj: "QRangeSlider"):
|
||||
qss = obj.styleSheet()
|
||||
p = obj
|
||||
while p.parent():
|
||||
qss = p.styleSheet() + qss
|
||||
p = p.parent()
|
||||
|
||||
parent = obj.parent()
|
||||
while parent is not None:
|
||||
qss = parent.styleSheet() + qss
|
||||
parent = parent.parent()
|
||||
qss = QApplication.instance().styleSheet() + qss
|
||||
|
||||
obj._style.has_stylesheet = False
|
||||
|
||||
# Find bar color
|
||||
# TODO: optional horizontal or vertical
|
||||
match = re.search(r"Slider::sub-page:?([^{\s]*)?\s*{\s*([^}]+)}", qss, re.S)
|
||||
if match:
|
||||
orientation, content = match.groups()
|
||||
for line in reversed(content.splitlines()):
|
||||
bgrd = re.search(r"background(-color)?:\s*([^;]+)", line)
|
||||
if bgrd:
|
||||
color = parse_color(bgrd.groups()[-1])
|
||||
obj._style.brush_active = color
|
||||
# TODO: parse for inactive and disabled
|
||||
obj._style.brush_inactive = color
|
||||
obj._style.brush_disabled = color
|
||||
obj._style.has_stylesheet = True
|
||||
class_name = type(obj).__name__
|
||||
_ss = f"\n{class_name}::sub-page:{orientation}{{background: none}}"
|
||||
# TODO: block double event
|
||||
obj.setStyleSheet(qss + _ss)
|
||||
break
|
||||
if not qss:
|
||||
return
|
||||
|
||||
# Find bar height/width
|
||||
for orient, dim in (("horizontal", "height"), ("vertical", "width")):
|
||||
|
@@ -1,10 +1,115 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from qtrangeslider import QRangeSlider
|
||||
from qtrangeslider.qtcompat.QtCore import Qt
|
||||
|
||||
WINDOWS = os.name == "nt"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("orientation", ["Horizontal", "Vertical"])
|
||||
def test_basic(qtbot, orientation):
|
||||
rs = QRangeSlider(getattr(Qt, orientation))
|
||||
qtbot.addWidget(rs)
|
||||
|
||||
|
||||
# @pytest.mark.skipif(WINDOWS, reason="QTest.mouseMove not working on windows")
|
||||
# def test_drag_handles(qtbot):
|
||||
# rs = QRangeSlider(Qt.Horizontal)
|
||||
# qtbot.addWidget(rs)
|
||||
# rs.setRange(0, 99)
|
||||
# rs.setValue((20, 80))
|
||||
# rs.setMouseTracking(True)
|
||||
# rs.show()
|
||||
|
||||
# # press the left handle
|
||||
# opt = rs._getStyleOption()
|
||||
# pos = rs._handleRects(opt, 0).center()
|
||||
# with qtbot.waitSignal(rs.sliderPressed):
|
||||
# qtbot.mousePress(rs, Qt.LeftButton, pos=pos)
|
||||
# assert rs._pressedControl == ("handle", 0)
|
||||
|
||||
# # drag the left handle
|
||||
# with qtbot.waitSignals([rs.sliderMoved] * 13): # couple less signals
|
||||
# for _ in range(15):
|
||||
# pos.setX(pos.x() + 2)
|
||||
# qtbot.mouseMove(rs, pos)
|
||||
|
||||
# with qtbot.waitSignal(rs.sliderReleased):
|
||||
# qtbot.mouseRelease(rs, Qt.LeftButton)
|
||||
|
||||
# # check the values
|
||||
# assert rs.value()[0] > 30
|
||||
# assert rs._pressedControl == rs._NULL_CTRL
|
||||
|
||||
# # press the right handle
|
||||
# pos = rs._handleRects(opt, 1).center()
|
||||
# with qtbot.waitSignal(rs.sliderPressed):
|
||||
# qtbot.mousePress(rs, Qt.LeftButton, pos=pos)
|
||||
# assert rs._pressedControl == ("handle", 1)
|
||||
|
||||
# # drag the right handle
|
||||
# with qtbot.waitSignals([rs.sliderMoved] * 13): # couple less signals
|
||||
# for _ in range(15):
|
||||
# pos.setX(pos.x() - 2)
|
||||
# qtbot.mouseMove(rs, pos)
|
||||
# with qtbot.waitSignal(rs.sliderReleased):
|
||||
# qtbot.mouseRelease(rs, Qt.LeftButton)
|
||||
|
||||
# # check the values
|
||||
# assert rs.value()[1] < 70
|
||||
# assert rs._pressedControl == rs._NULL_CTRL
|
||||
|
||||
|
||||
# @pytest.mark.skipif(WINDOWS, reason="QTest.mouseMove not working on windows")
|
||||
# def test_drag_handles_beyond_edge(qtbot):
|
||||
# rs = QRangeSlider(Qt.Horizontal)
|
||||
# qtbot.addWidget(rs)
|
||||
# rs.setRange(0, 99)
|
||||
# rs.setValue((20, 80))
|
||||
# rs.setMouseTracking(True)
|
||||
# rs.show()
|
||||
|
||||
# # press the right handle
|
||||
# opt = rs._getStyleOption()
|
||||
# pos = rs._handleRects(opt, 1).center()
|
||||
# with qtbot.waitSignal(rs.sliderPressed):
|
||||
# qtbot.mousePress(rs, Qt.LeftButton, pos=pos)
|
||||
# assert rs._pressedControl == ("handle", 1)
|
||||
|
||||
# # drag the handle off the right edge and make sure the value gets to the max
|
||||
# for _ in range(5):
|
||||
# pos.setX(pos.x() + 20)
|
||||
# qtbot.mouseMove(rs, pos)
|
||||
|
||||
# with qtbot.waitSignal(rs.sliderReleased):
|
||||
# qtbot.mouseRelease(rs, Qt.LeftButton)
|
||||
|
||||
# assert rs.value()[1] == 99
|
||||
|
||||
|
||||
# @pytest.mark.skipif(WINDOWS, reason="QTest.mouseMove not working on windows")
|
||||
# def test_bar_drag_beyond_edge(qtbot):
|
||||
# rs = QRangeSlider(Qt.Horizontal)
|
||||
# qtbot.addWidget(rs)
|
||||
# rs.setRange(0, 99)
|
||||
# rs.setValue((20, 80))
|
||||
# rs.setMouseTracking(True)
|
||||
# rs.show()
|
||||
|
||||
# # press the right handle
|
||||
# pos = rs.rect().center()
|
||||
# with qtbot.waitSignal(rs.sliderPressed):
|
||||
# qtbot.mousePress(rs, Qt.LeftButton, pos=pos)
|
||||
# assert rs._pressedControl == ("bar", 1)
|
||||
|
||||
# # drag the handle off the right edge and make sure the value gets to the max
|
||||
# for _ in range(15):
|
||||
# pos.setX(pos.x() + 10)
|
||||
# qtbot.mouseMove(rs, pos)
|
||||
|
||||
# with qtbot.waitSignal(rs.sliderReleased):
|
||||
# qtbot.mouseRelease(rs, Qt.LeftButton)
|
||||
|
||||
# assert rs.value()[1] == 99
|
||||
|
@@ -22,7 +22,7 @@ elif PYQT6:
|
||||
|
||||
# backwards compat with PyQt5
|
||||
# namespace moves:
|
||||
for cls in (QStyle, QSlider):
|
||||
for cls in (QStyle, QSlider, QSizePolicy, QSpinBox):
|
||||
for attr in dir(cls):
|
||||
if not attr[0].isupper():
|
||||
continue
|
||||
|
@@ -45,8 +45,7 @@ testing =
|
||||
tox
|
||||
tox-conda
|
||||
pytest
|
||||
# https://github.com/pytest-dev/pytest-qt/pull/340
|
||||
pytest-qt @ git+https://github.com/The-Compiler/pytest-qt.git@pyqt6
|
||||
pytest-qt
|
||||
pytest-cov
|
||||
dev =
|
||||
ipython
|
||||
|