source: sasview/src/sas/qtgui/Plotting/rangeSlider.py @ 4e887d8

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 4e887d8 was 7969b9c, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

More functionality conversion

  • Property mode set to 100644
File size: 11.0 KB
Line 
1# -*- coding: utf-8 -*-
2
3"""
4/***************************************************************************
5Name                 : RangeSlider
6Description          : A slider for ranges
7Date                 : Jun 20, 2012
8copyright            : (C) 2012 by Giuseppe Sucameli
9email                : brush.tyler@gmail.com
10
11the code is based on RangeSlider by phil
12(see https://svn.enthought.com/enthought/browser/TraitsBackendQt/trunk/enthought/traits/ui/qt4/extra/range_slider.py) licensed under GPLv2
13 ***************************************************************************/
14
15/***************************************************************************
16 *                                                                         *
17 *   This program is free software; you can redistribute it and/or modify  *
18 *   it under the terms of the GNU General Public License as published by  *
19 *   the Free Software Foundation; either version 2 of the License, or     *
20 *   (at your option) any later version.                                   *
21 *                                                                         *
22 ***************************************************************************/
23"""
24
25from PyQt5 import QtCore
26from PyQt5 import QtGui
27from PyQt5 import QtWidgets
28
29class RangeSlider(QtWidgets.QSlider):
30    """ A slider for ranges.
31   
32        This class provides a dual-slider for ranges, where there is a defined
33        maximum and minimum, as is a normal slider, but instead of having a
34        single slider value, there are 2 slider values.
35       
36        This class emits the same signals as the QSlider base class, with the
37        exception of valueChanged.
38
39        In addition, two new signals are emitted to catch the movement of
40        each handle, lowValueChanged(int) and highValueChanged(int).
41    """
42    highValueChanged = QtCore.pyqtSignal(int)
43    lowValueChanged = QtCore.pyqtSignal(int)
44    def __init__(self, *args):
45        super(RangeSlider, self).__init__(*args)
46
47        self._low = self.minimum()
48        self._high = self.maximum()
49       
50        self.pressed_control = QtWidgets.QStyle.SC_None
51        self.hover_control = QtWidgets.QStyle.SC_None
52        self.click_offset = 0
53       
54        # -1 for the low, 1 for the high, 0 for both
55        self.active_slider = -1
56
57    def lowValue(self):
58        return self._low
59
60    def setLowValue(self, low):
61        if low < self.minimum():
62            low = self.minimum()
63        if low == self._low:
64            return
65
66        self._low = low
67        self.update()
68
69        if self.hasTracking():
70            self.lowValueChanged.emit(self._low)
71
72    def highValue(self):
73        return self._high
74
75    def setHighValue(self, high):
76        if high > self.maximum():
77            high = self.maximum()
78        if high == self._high:
79            return
80
81        self._high = high
82        self.update()
83
84        if self.hasTracking():
85            self.lowValueChanged.emit(self._high)
86       
87    def paintEvent(self, event):
88        # based on http://qt.gitorious.org/qt/qt/blobs/master/src/gui/widgets/qslider.cpp
89
90        painter = QtGui.QPainter(self)
91        style = QtWidgets.QApplication.style()
92
93        for i, value in enumerate([self._low, self._high]):
94            opt = QtWidgets.QStyleOptionSlider()
95            self.initStyleOption(opt)
96
97            gr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderGroove, self)
98            sr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderHandle, self)
99
100            if self.orientation() == QtCore.Qt.Horizontal:
101                handle_length = sr.width()
102                slider_length = gr.width()
103                slider_min = gr.x()
104                slider_max = gr.right() - handle_length + 1
105            else:
106                handle_length = sr.height()
107                slider_length = gr.height()
108                slider_min = gr.y()
109                slider_max = gr.bottom() - handle_length + 1
110
111            opt.subControls = QtWidgets.QStyle.SC_SliderGroove | QtWidgets.QStyle.SC_SliderHandle
112
113            # draw the first slider with inverted appearance, then the second
114            # slider drawn on top of the existing ones
115            if i == 0:
116                if self.orientation() == QtCore.Qt.Horizontal:
117                    opt.upsideDown = not self.invertedAppearance()
118                else:
119                    opt.upsideDown = self.invertedAppearance()
120
121                if self.orientation() == QtCore.Qt.Horizontal:
122                    opt.sliderValue = value - self.minimum()
123                    opt.sliderPosition = self.minimum() + (self.maximum() - value)
124
125                    opt.rect.setX(slider_min)
126                    opt.rect.setWidth(slider_length)
127
128                else:
129                    opt.rect.setY(slider_min)
130                    opt.rect.setHeight(slider_length)
131
132            else:
133                # do not highlight the second part when has focus to avoid
134                # drawing of partially overlapped semi-transparent backgrounds
135                opt.state &= ~QtWidgets.QStyle.State_HasFocus
136
137                opt.sliderValue = 0
138                opt.sliderPosition = self.minimum()
139
140                if self.orientation() == QtCore.Qt.Horizontal:
141                    opt.upsideDown = self.invertedAppearance()
142
143                    pos = style.sliderPositionFromValue(0, self.maximum()-self.minimum(),
144                                             value-self.minimum(), slider_max - slider_min - 4,
145                                             opt.upsideDown)
146
147                    opt.rect.setX(pos + 3)
148                    opt.rect.setWidth(slider_max - pos + handle_length - 3)
149
150                else:
151                    opt.upsideDown = not self.invertedAppearance()
152
153                    pos = style.sliderPositionFromValue(0, self.maximum()-self.minimum(),
154                                             self.maximum()-value, slider_max - slider_min - 4,
155                                             opt.upsideDown)
156
157                    opt.rect.setY(slider_min-1)
158                    opt.rect.setHeight(slider_min + slider_length - pos - 2)
159
160            if self.tickPosition() != self.NoTicks:
161                opt.subControls |= QtWidgets.QStyle.SC_SliderTickmarks
162
163            if self.pressed_control:
164                opt.activeSubControls = self.pressed_control
165                opt.state |= QtWidgets.QStyle.State_Sunken
166            else:
167                opt.activeSubControls = self.hover_control
168
169            style.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt, painter, self)
170           
171       
172    def mousePressEvent(self, event):
173        event.accept()
174       
175        style = QtWidgets.QApplication.style()
176        button = event.button()
177       
178        # In a normal slider control, when the user clicks on a point in the
179        # slider's total range, but not on the slider part of the control the
180        # control would jump the slider value to where the user clicked.
181        # For this control, clicks which are not direct hits will slide both
182        # slider parts
183               
184        if button:
185            opt = QtWidgets.QStyleOptionSlider()
186            self.initStyleOption(opt)
187
188            self.active_slider = 0
189            mid = (self.maximum() - self.minimum()) / 2.0
190           
191            for i, value in enumerate([self._low, self._high]):
192                opt.sliderPosition = value
193                hit = style.hitTestComplexControl(style.CC_Slider, opt, event.pos(), self)
194
195                if hit == style.SC_SliderHandle:
196                    self.pressed_control = hit
197
198                    # if both handles are close together, avoid locks
199                    # choosing the one with more empty space near it
200                    if self._low + 2 >= self._high:
201                        self.active_slider = 1 if self._high < mid else -1
202                    else:
203                        self.active_slider = -1 if i == 0 else 1
204                    self.triggerAction(self.SliderMove)
205                    self.setRepeatAction(self.SliderNoAction)
206                    self.setSliderDown(True)
207                    break
208
209            if self.active_slider == 0:
210                self.pressed_control = QtWidgets.QStyle.SC_SliderHandle
211                self.click_offset = self.__pixelPosToRangeValue(self.__pick(event.pos()))
212                self.triggerAction(self.SliderMove)
213                self.setRepeatAction(self.SliderNoAction)
214                self.setSliderDown(True)
215        else:
216            event.ignore()
217
218    def mouseReleaseEvent(self, event):
219        if self.pressed_control != QtWidgets.QStyle.SC_SliderHandle:
220            event.ignore()
221            return
222
223        self.setSliderDown(False)
224        return QtWidgets.QSlider.mouseReleaseEvent(self, event)
225                               
226    def mouseMoveEvent(self, event):
227        if self.pressed_control != QtWidgets.QStyle.SC_SliderHandle:
228            event.ignore()
229            return
230       
231        event.accept()
232        new_pos = self.__pixelPosToRangeValue(self.__pick(event.pos()))
233        opt = QtWidgets.QStyleOptionSlider()
234        self.initStyleOption(opt)
235
236        old_click_offset = self.click_offset
237        self.click_offset = new_pos
238       
239        if self.active_slider == 0:
240            offset = new_pos - old_click_offset
241            new_low = self._low + offset
242            new_high = self._high + offset
243
244            if new_low < self.minimum():
245                diff = self.minimum() - new_low
246                new_low += diff
247                new_high += diff
248            if new_high > self.maximum():
249                diff = self.maximum() - new_high
250                new_low += diff
251                new_high += diff
252
253            self.setLowValue( new_low )
254            self.setHighValue( new_high )
255
256        elif self.active_slider < 0:
257            if new_pos > self._high:
258                new_pos = self._high
259            self.setLowValue( new_pos )
260
261        else:
262            if new_pos < self._low:
263                new_pos = self._low
264            self.setHighValue( new_pos )
265
266           
267    def __pick(self, pt):
268        if self.orientation() == QtCore.Qt.Horizontal:
269            return pt.x()
270        else:
271            return pt.y()
272           
273           
274    def __pixelPosToRangeValue(self, pos):
275        opt = QtWidgets.QStyleOptionSlider()
276        self.initStyleOption(opt)
277        style = QtWidgets.QApplication.style()
278       
279        gr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderGroove, self)
280        sr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderHandle, self)
281       
282        if self.orientation() == QtCore.Qt.Horizontal:
283            handle_length = sr.width()
284            slider_min = gr.x() + handle_length/2
285            slider_max = gr.right() - handle_length/2 + 1
286        else:
287            handle_length = sr.height()
288            slider_min = gr.y() + handle_length/2
289            slider_max = gr.bottom() - handle_length/2 + 1
290           
291        return self.minimum() + style.sliderValueFromPosition(0, self.maximum()-self.minimum(),
292                                            pos-slider_min, slider_max - slider_min,
293                                            opt.upsideDown)
Note: See TracBrowser for help on using the repository browser.