class QSpanSlider(QtGui.QSlider): # Based on QxtSpanSlider.py (https://github.com/mkilling/QxtSpanSlider.py) # The MIT License (MIT) # Copyright (c) 2011-2014 Marvin Killing # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. spanChanged = QtCore.Signal(int, int) upperPositionChanged = QtCore.Signal(int) lowerPositionChanged = QtCore.Signal(int) sliderPressed = QtCore.Signal(int) NoHandle = None LowerHandle = 1 UpperHandle = 2 def __init__(self, orientation=QtCore.Qt.Horizontal, parent=None): QtGui.QSlider.__init__(self, orientation, parent) # self.connect(self, SIGNAL("rangeChanged(int, int)"), self.updateRange) self.connect(self, QtCore.SIGNAL("sliderReleased()"), self.movePressedHandle) # self.setStyle(QStyleFactory.create('Plastique')) self.lower = 0 self.upper = 0 self.lowerPos = 0 self.upperPos = 0 self.offset = 0 self.position = 0 self.lastPressed = QSpanSlider.NoHandle self.upperPressed = QtGui.QStyle.SC_None self.lowerPressed = QtGui.QStyle.SC_None # self.movement = QSpanSlider.FreeMovement self.mainControl = QSpanSlider.LowerHandle self.firstMovement = False self.blockTracking = False self.gradientLeft = self.palette().color(QtGui.QPalette.Dark).lighter(110) self.gradientRight = self.palette().color(QtGui.QPalette.Dark).lighter(110) def lowerValue(self): return min(self.lower, self.upper) def setLowerValue(self, lower): self.setSpan(lower, self.upper) def upperValue(self): return max(self.lower, self.upper) def setUpperValue(self, upper): self.setSpan(self.lower, upper) def setSpan(self, lower, upper): low = min(self.maximum(), max(self.minimum(), min(lower, upper))) upp = min(self.maximum(), max(self.minimum(), max(lower, upper))) changed = False if low != self.lower: self.lower = low self.lowerPos = low changed = True if upp != self.upper: self.upper = upp self.upperPos = upp changed = True if changed: self.spanChanged.emit(self.lower, self.upper) self.update() def lowerPosition(self): return self.lowerPos def setLowerPosition(self, lower): if self.lowerPos != lower: self.lowerPos = lower if not self.hasTracking(): self.update() if self.isSliderDown(): self.lowerPositionChanged.emit(lower) if self.hasTracking() and not self.blockTracking: main = (self.mainControl == QSpanSlider.LowerHandle) self.triggerAction(QSpanSlider.SliderMove, main) def upperPosition(self): return self.upperPos def setUpperPosition(self, upper): if self.upperPos != upper: self.upperPos = upper if not self.hasTracking(): self.update() if self.isSliderDown(): self.upperPositionChanged.emit(upper) if self.hasTracking() and not self.blockTracking: main = (self.mainControl == QSpanSlider.UpperHandle) self.triggerAction(QSpanSlider.SliderMove, main) def gradientLeftColor(self): return self.gradientLeft def setGradientLeftColor(self, color): self.gradientLeft = color self.update() def gradientRightColor(self): return self.gradientRight def setGradientRightColor(self, color): self.gradientRight = color self.update() def movePressedHandle(self): if self.lastPressed == QSpanSlider.LowerHandle: if self.lowerPos != self.lower: main = (self.mainControl == QSpanSlider.LowerHandle) self.triggerAction(QtGui.QAbstractSlider.SliderMove, main) elif self.lastPressed == QSpanSlider.UpperHandle: if self.upperPos != self.upper: main = (self.mainControl == QSpanSlider.UpperHandle) self.triggerAction(QtGui.QAbstractSlider.SliderMove, main) def pick(self, p): if self.orientation() == QtCore.Qt.Horizontal: return p.x() else: return p.y() def triggerAction(self, action, main): value = 0 no = False up = False my_min = self.minimum() my_max = self.maximum() altControl = QSpanSlider.LowerHandle if self.mainControl == QSpanSlider.LowerHandle: altControl = QSpanSlider.UpperHandle self.blockTracking = True isUpperHandle = (main and self.mainControl == QSpanSlider.UpperHandle) or (not main and altControl == QSpanSlider.UpperHandle) if action == QtGui.QAbstractSlider.SliderSingleStepAdd: if isUpperHandle: value = min(my_max, max(my_min, self.upper + self.singleStep())) up = True else: value = min(my_max, max(my_min, self.lower + self.singleStep())) elif action == QtGui.QAbstractSlider.SliderSingleStepSub: if isUpperHandle: value = min(my_max, max(my_min, self.upper - self.singleStep())) up = True else: value = min(my_max, max(my_min, self.lower - self.singleStep())) elif action == QtGui.QAbstractSlider.SliderToMinimum: value = my_min if isUpperHandle: up = True elif action == QtGui.QAbstractSlider.SliderToMaximum: value = my_max if isUpperHandle: up = True elif action == QtGui.QAbstractSlider.SliderMove: if isUpperHandle: up = True no = True elif action == QtGui.QAbstractSlider.SliderNoAction: no = True if not no and not up: # if self.movement == QSpanSlider.NoCrossing: # value = min(value, self.upper) # elif self.movement == QSpanSlider.NoOverlapping: value = min(value, self.upper) # if self.movement == QSpanSlider.FreeMovement and value > self.upper: # self.swapControls() # self.setUpperPosition(value) # else: self.setLowerPosition(value) elif not no: # if self.movement == QSpanSlider.NoCrossing: # value = max(value, self.lower) # elif self.movement == QSpanSlider.NoOverlapping: value = max(value, self.lower) # if self.movement == QSpanSlider.FreeMovement and value < self.lower: # self.swapControls() # self.setLowerPosition(value) # else: self.setUpperPosition(value) self.blockTracking = False self.setLowerValue(self.lowerPos) self.setUpperValue(self.upperPos) def paintEvent(self, event): painter = QtGui.QStylePainter(self) # ticks opt = QtGui.QStyleOptionSlider() self.initStyleOption(opt) # opt.subControls = QtGui.QStyle.SC_SliderTickmarks painter.drawComplexControl(QtGui.QStyle.CC_Slider, opt) # groove opt.sliderPosition = 20 opt.sliderValue = 0 opt.subControls = QtGui.QStyle.SC_SliderGroove painter.drawComplexControl(QtGui.QStyle.CC_Slider, opt) # handle rects opt.sliderPosition = self.lowerPos lr = self.style().subControlRect(QtGui.QStyle.CC_Slider, opt, QtGui.QStyle.SC_SliderHandle, self) lrv = self.pick(lr.center()) opt.sliderPosition = self.upperPos ur = self.style().subControlRect(QtGui.QStyle.CC_Slider, opt, QtGui.QStyle.SC_SliderHandle, self) urv = self.pick(ur.center()) # span minv = min(lrv, urv) maxv = max(lrv, urv) c = self.style().subControlRect(QtGui.QStyle.CC_Slider, opt, QtGui.QStyle.SC_SliderGroove, self).center() spanRect = QtCore.QRect(QtCore.QPoint(c.x() - 2, minv), QtCore.QPoint(c.x() + 1, maxv)) if self.orientation() == QtCore.Qt.Horizontal: spanRect = QtCore.QRect(QtCore.QPoint(minv, c.y() - 2), QtCore.QPoint(maxv, c.y() + 1)) self.drawSpan(painter, spanRect) # handles if self.lastPressed == QSpanSlider.LowerHandle: self.drawHandle(painter, QSpanSlider.UpperHandle) self.drawHandle(painter, QSpanSlider.LowerHandle) else: self.drawHandle(painter, QSpanSlider.LowerHandle) self.drawHandle(painter, QSpanSlider.UpperHandle) def setupPainter(self, painter, orientation, x1, y1, x2, y2): highlight = self.palette().color(QtGui.QPalette.Highlight) gradient = QtGui.QLinearGradient(x1, y1, x2, y2) gradient.setColorAt(0, highlight.lighter(108)) gradient.setColorAt(1, highlight.lighter(108)) painter.setBrush(gradient) if orientation == QtCore.Qt.Horizontal: painter.setPen(QtGui.QPen(highlight.darker(130), 0)) else: painter.setPen(QtGui.QPen(highlight.darker(150), 0)) def drawSpan(self, painter, rect): opt = QtGui.QStyleOptionSlider() QtGui.QSlider.initStyleOption(self, opt) # area groove = self.style().subControlRect(QtGui.QStyle.CC_Slider, opt, QtGui.QStyle.SC_SliderGroove, self) if opt.orientation == QtCore.Qt.Horizontal: groove.adjust(0, 0, -1, 0) else: groove.adjust(0, 0, 0, -1) # pen & brush painter.setPen(QtGui.QPen(self.gradientLeft, 0)) if opt.orientation == QtCore.Qt.Horizontal: self.setupPainter(painter, opt.orientation, groove.center().x(), groove.top(), groove.center().x(), groove.bottom()) else: self.setupPainter(painter, opt.orientation, groove.left(), groove.center().y(), groove.right(), groove.center().y()) # draw groove intersected = QtCore.QRectF(rect.intersected(groove)) gradient = QtGui.QLinearGradient(intersected.topLeft(), intersected.topRight()) gradient.setColorAt(0, self.gradientLeft) gradient.setColorAt(1, self.gradientRight) painter.fillRect(intersected, gradient) def drawHandle(self, painter, handle): opt = QtGui.QStyleOptionSlider() self._initStyleOption(opt, handle) opt.subControls = QtGui.QStyle.SC_SliderHandle # if handle == QSpanSlider.LowerHandle: # print "LowerHandle (",self.lowerPressed,")" # elif handle == QSpanSlider.UpperHandle: # print "UpperHandle (",self.upperPressed,")" # else: # print "NoHandle" pressed = self.upperPressed if handle == QSpanSlider.LowerHandle: pressed = self.lowerPressed if pressed == QtGui.QStyle.SC_SliderHandle: opt.activeSubControls = pressed opt.state |= QtGui.QStyle.State_Sunken painter.drawComplexControl(QtGui.QStyle.CC_Slider, opt) def _initStyleOption(self, option, handle): self.initStyleOption(option) option.sliderPosition = self.lowerPos if handle == QSpanSlider.UpperHandle: option.sliderPosition = self.upperPos option.sliderValue = self.lower if handle == QSpanSlider.UpperHandle: option.sliderValue = self.upper def handleMousePress(self, pos, control, value, handle): opt = QtGui.QStyleOptionSlider() self._initStyleOption(opt, handle) oldControl = control control = self.style().hitTestComplexControl(QtGui.QStyle.CC_Slider, opt, pos, self) sr = self.style().subControlRect(QtGui.QStyle.CC_Slider, opt, QtGui.QStyle.SC_SliderHandle, self) if control == QtGui.QStyle.SC_SliderHandle: self.position = value self.offset = self.pick(pos - sr.topLeft()) self.lastPressed = handle self.setSliderDown(True) # self.sliderPressed.emit(handle) self.emit(QtCore.SIGNAL("sliderPressed(PyQt_PyObject)"), handle) if control != oldControl: self.update(sr) return control def mousePressEvent(self, event): if self.minimum() == self.maximum() or event.buttons() ^ event.button(): event.ignore() return self.lowerPressed = self.handleMousePress(event.pos(), self.lowerPressed, self.lower, QSpanSlider.LowerHandle) self.upperPressed = self.handleMousePress(event.pos(), self.upperPressed, self.upper, QSpanSlider.UpperHandle) self.firstMovement = True event.accept() def mouseMoveEvent(self, event): if self.lowerPressed != QtGui.QStyle.SC_SliderHandle and self.upperPressed != QtGui.QStyle.SC_SliderHandle: event.ignore() return opt = QtGui.QStyleOptionSlider() self.initStyleOption(opt) m = self.style().pixelMetric(QtGui.QStyle.PM_MaximumDragDistance, opt, self) newPosition = self.pixelPosToRangeValue(self.pick(event.pos()) - self.offset) if m >= 0: r = self.rect().adjusted(-m, -m, m, m) if not r.contains(event.pos()): newPosition = self.position # pick the preferred handle on the first movement if self.firstMovement: # if self.lower == self.upper: # if newPosition < self.lowerValue(): # self.swapControls() # self.firstMovement = False # else: self.firstMovement = False if self.lowerPressed == QtGui.QStyle.SC_SliderHandle: newPosition = min(newPosition, self.upper) self.setLowerPosition(newPosition) elif self.upperPressed == QtGui.QStyle.SC_SliderHandle: newPosition = max(newPosition, self.lower) self.setUpperPosition(newPosition) event.accept() def mouseReleaseEvent(self, event): QtGui.QSlider.mouseReleaseEvent(self, event) self.setSliderDown(False) self.lowerPressed = QtGui.QStyle.SC_None self.upperPressed = QtGui.QStyle.SC_None self.update() def pixelPosToRangeValue(self, pos): opt = QtGui.QStyleOptionSlider() self.initStyleOption(opt) sliderMin = 0 sliderMax = 0 sliderLength = 0 gr = self.style().subControlRect(QtGui.QStyle.CC_Slider, opt, QtGui.QStyle.SC_SliderGroove, self) sr = self.style().subControlRect(QtGui.QStyle.CC_Slider, opt, QtGui.QStyle.SC_SliderHandle, self) if self.orientation() == QtCore.Qt.Horizontal: sliderLength = sr.width() sliderMin = gr.x() sliderMax = gr.right() - sliderLength + 1 else: sliderLength = sr.height() sliderMin = gr.y() sliderMax = gr.bottom() - sliderLength + 1 return QtGui.QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), pos - sliderMin, sliderMax - sliderMin, opt.upsideDown) # lowerValue = QtCore.pyqtProperty("int", lowerValue, setLowerValue) # upperValue = QtCore.pyqtProperty("int", upperValue, setUpperValue) upperPosition = QtCore.Property("int", upperPosition, setUpperPosition) lowerPosition = QtCore.Property("int", lowerPosition, setLowerPosition)