#!/usr/bin/env python3.8
# coding: latin-1
# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2022
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
cytoflowgui.editors.range_slider
--------------------------------
A ``QtGui.QSlider`` with two carets -- allows a range to be specified.
"""
from pyface.qt import QtGui, QtCore
[docs]class RangeSlider(QtGui.QSlider):
""" A slider for ranges.
This class provides a dual-slider for ranges, where there is a defined
maximum and minimum, as is a normal slider, but instead of having a
single slider value, there are 2 slider values.
This class emits the same signals as the ``QtGui.QSlider`` base class, with the
exception of ``valueChanged``
"""
def __init__(self, *args):
super(RangeSlider, self).__init__(*args)
self._low = self.minimum()
self._high = self.maximum()
self.pressed_control = QtGui.QStyle.SC_None
self.hover_control = QtGui.QStyle.SC_None
self.click_offset = 0
# 0 for the low, 1 for the high, -1 for both
self.active_slider = 0
self.last_active_slider = 0
[docs] def low(self):
return self._low
[docs] def setLow(self, low):
self._low = low
self.last_active_slider = 0
self.update()
[docs] def high(self):
return self._high
[docs] def setHigh(self, high):
self._high = high
self.last_active_slider = 1
self.update()
[docs] def sliderOrderForPaint(self):
if self.last_active_slider == 0:
return [(1, self._high), (0, self._low)]
else:
return [(0, self._low), (1, self._high)]
[docs] def paintEvent(self, event):
# based on http://qt.gitorious.org/qt/qt/blobs/master/src/gui/widgets/qslider.cpp
painter = QtGui.QPainter(self)
style = QtGui.QApplication.style()
hasPainted = False
for _, value in self.sliderOrderForPaint():
opt = QtGui.QStyleOptionSlider()
self.initStyleOption(opt)
# Only draw the groove for the first slider so it doesn't get drawn
# on top of the existing ones every time
if not hasPainted:
opt.subControls = QtGui.QStyle.SC_SliderGroove | QtGui.QStyle.SC_SliderHandle
hasPainted = True
else:
opt.subControls = QtGui.QStyle.SC_SliderHandle
if self.tickPosition() != self.NoTicks:
opt.subControls |= QtGui.QStyle.SC_SliderTickmarks
if self.pressed_control:
opt.activeSubControls = self.pressed_control
opt.state |= QtGui.QStyle.State_Sunken
else:
opt.activeSubControls = self.hover_control
opt.sliderPosition = value
opt.sliderValue = value
style.drawComplexControl(QtGui.QStyle.CC_Slider, opt, painter, self)
[docs] def mousePressEvent(self, event):
event.accept()
style = QtGui.QApplication.style()
button = event.button()
# In a normal slider control, when the user clicks on a point in the
# slider's total range, but not on the slider part of the control the
# control would jump the slider value to where the user clicked.
# For this control, clicks which are not direct hits will slide both
# slider parts
if button:
opt = QtGui.QStyleOptionSlider()
self.initStyleOption(opt)
self.active_slider = -1
for i, value in self.sliderOrderForMove():
opt.sliderPosition = value
hit = style.hitTestComplexControl(style.CC_Slider, opt, event.pos(), self)
if hit == style.SC_SliderHandle:
self.active_slider = i
self.last_active_slider = i
self.pressed_control = hit
self.triggerAction(self.SliderMove)
self.setRepeatAction(self.SliderNoAction)
self.setSliderDown(True)
break
if self.active_slider < 0:
self.pressed_control = QtGui.QStyle.SC_SliderHandle
self.click_offset = self.__pixelPosToRangeValue(self.__pick(event.pos()))
self.triggerAction(self.SliderMove)
self.setRepeatAction(self.SliderNoAction)
else:
event.ignore()
[docs] def mouseMoveEvent(self, event):
if self.pressed_control != QtGui.QStyle.SC_SliderHandle:
event.ignore()
return
event.accept()
new_pos = self.__pixelPosToRangeValue(self.__pick(event.pos()))
opt = QtGui.QStyleOptionSlider()
self.initStyleOption(opt)
if self.active_slider < 0:
offset = new_pos - self.click_offset
self._high += offset
self._low += offset
if self._low < self.minimum():
diff = self.minimum() - self._low
self._low += diff
self._high += diff
if self._high > self.maximum():
diff = self.maximum() - self._high
self._low += diff
self._high += diff
elif self.active_slider == 0:
if new_pos >= self._high:
new_pos = self._high - 1
self._low = new_pos
else:
if new_pos <= self._low:
new_pos = self._low + 1
self._high = new_pos
self.click_offset = new_pos
self.update()
self.sliderMoved.emit(new_pos)
[docs] def mouseReleaseEvent(self, event):
event.accept()
self.sliderReleased.emit()
def __pick(self, pt):
if self.orientation() == QtCore.Qt.Horizontal:
return pt.x()
else:
return pt.y()
def __pixelPosToRangeValue(self, pos):
opt = QtGui.QStyleOptionSlider()
self.initStyleOption(opt)
style = QtGui.QApplication.style()
gr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderGroove, self)
sr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderHandle, self)
if self.orientation() == QtCore.Qt.Horizontal:
slider_length = sr.width()
slider_min = gr.x()
slider_max = gr.right() - slider_length + 1
else:
slider_length = sr.height()
slider_min = gr.y()
slider_max = gr.bottom() - slider_length + 1
return style.sliderValueFromPosition(self.minimum(), self.maximum(),
pos-slider_min, slider_max-slider_min,
opt.upsideDown)
if __name__ == "__main__":
import sys
def echo(value):
print(value)
app = QtGui.QApplication(sys.argv)
slider = RangeSlider()
slider.setMinimum(0)
slider.setMaximum(10000)
slider.setLow(0)
slider.setHigh(10000)
slider.sliderMoved.connect(echo)
slider.show()
app.exec_()