451 lines
15 KiB
Python
451 lines
15 KiB
Python
from PyQt5 import QtWidgets
|
|
from PyQt5 import QtCore
|
|
from PyQt5 import QtGui
|
|
from PyQt5.QtCore import QSize, QFileInfo, pyqtSlot
|
|
from PyQt5.QtGui import QIcon, QKeySequence
|
|
from PyQt5.QtMultimedia import QMediaPlayer
|
|
|
|
import sys
|
|
|
|
__all__ = ['QRangeSlider']
|
|
|
|
|
|
def scale(val, src, dst):
|
|
"""
|
|
Scale the given value from the scale of src to the scale of dst.
|
|
"""
|
|
return int(((val - src[0]) / float(src[1] - src[0])) * (dst[1] - dst[0]) + dst[0])
|
|
|
|
|
|
class WidgetTimeLine(QtWidgets.QWidget):
|
|
|
|
signalSetPosition = QtCore.pyqtSignal(int)
|
|
signalPlay = QtCore.pyqtSignal()
|
|
signalStop = QtCore.pyqtSignal()
|
|
signalSelectID = QtCore.pyqtSignal(list)
|
|
|
|
def __init__(self, parent=None):
|
|
super(WidgetTimeLine, self).__init__(parent)
|
|
self.root = QFileInfo(__file__).absolutePath()
|
|
|
|
self.setFixedHeight(110)
|
|
self.frame = 0
|
|
|
|
layout = QtWidgets.QGridLayout()
|
|
|
|
iconSize = QSize(28, 28)
|
|
|
|
self.playButton = QtWidgets.QToolButton()
|
|
self.playButton.setStyleSheet('border: none;')
|
|
self.playButton.setIcon(QIcon(self.root + '/../icons/play.png'))
|
|
self.playButton.setIconSize(iconSize)
|
|
self.playButton.setToolTip("Play movie")
|
|
self.playButton.clicked.connect(self.play)
|
|
|
|
# See https://stackoverflow.com/questions/50880660/how-to-change-space-bar-behaviour-in-pyqt5-python3
|
|
# self.shortcut_play = QtWidgets.QShortcut(QKeySequence(Qt.Key_Space), self)
|
|
# self.shortcut_play.activated.connect(self.play)
|
|
|
|
self.labelFrame = QtWidgets.QLabel('Frame\n')
|
|
self.labelFrame.setAlignment(QtCore.Qt.AlignRight)
|
|
self.labelFrame.setFixedWidth(40)
|
|
self.sl = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
|
# Directly connect: Slider value changed to player set position
|
|
self.sl.valueChanged.connect(self.signalSetPosition.emit)
|
|
|
|
self.rangeslider = QMultiRangeSlider()
|
|
self.rangeslider.setFixedHeight(30)
|
|
self.rangeslider.setMin(0)
|
|
self.rangeslider.setMax(200)
|
|
self.rangeslider.setRanges([(5, 25), (30, 50), (70, 90)])
|
|
# self.rangeslider.setBackgroundStyle(
|
|
# 'background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);')
|
|
# szlf.rangeslider.setHandleStyle(
|
|
# 'background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #289, stop:1 #289);')
|
|
|
|
layout.addWidget(self.rangeslider, 1, 1, 1, 6)
|
|
layout.addWidget(self.labelFrame, 1, 0, 2, 1)
|
|
layout.addWidget(self.sl, 2, 1, 1, 6)
|
|
layout.addWidget(self.playButton, 2, 0, 1, 1)
|
|
|
|
self.setLayout(layout)
|
|
self.number_ids = None
|
|
self.fps = None
|
|
|
|
def setInit(self, frames, number_ids, fps):
|
|
self.fps = fps
|
|
self.frame = 0
|
|
self.sl.setMinimum(0)
|
|
# Use position = (frame * 1000) / FPS for all slider and player values!
|
|
self.sl.setMaximum(int((frames * 1000) / self.fps))
|
|
self.sl.setValue(0)
|
|
self.sl.setTickPosition(QtWidgets.QSlider.TicksBelow)
|
|
self.sl.setTickInterval(5)
|
|
self.labelFrame.setText('Frame\n %i' % self.frame)
|
|
self.playButton.setEnabled(True)
|
|
self.number_ids = number_ids
|
|
|
|
self.rangeslider.setMin(0)
|
|
self.rangeslider.setMax(frames)
|
|
self.rangeslider.setRanges([(0, frames)])
|
|
|
|
@pyqtSlot(QMediaPlayer.State)
|
|
def mediaStateChanged(self, state):
|
|
if state == QMediaPlayer.PlayingState:
|
|
print('MediaState changed...')
|
|
self.playButton.setIcon(QIcon(self.root + '/../icons/pause.png'))
|
|
self.playButton.setToolTip('Pause movie')
|
|
else:
|
|
self.playButton.setIcon(QIcon(self.root + '/../icons/play.png'))
|
|
self.playButton.setToolTip('Play movie')
|
|
|
|
def play(self):
|
|
self.signalPlay.emit()
|
|
|
|
def stop(self):
|
|
self.signalStop.emit()
|
|
|
|
def updateSlider(self, position):
|
|
# Directly connect Player position changed to slider set value
|
|
frame = int((position / 1000.0) * self.fps)
|
|
self.frame = frame
|
|
# Disable the events to prevent updating triggering a setPosition event (can cause stuttering).
|
|
self.sl.blockSignals(True)
|
|
self.sl.setValue(position)
|
|
self.sl.blockSignals(False)
|
|
|
|
self.labelFrame.setText('Frame\n %i' % frame)
|
|
|
|
|
|
class QMultiRangeSlider(QtWidgets.QWidget):
|
|
|
|
minValueChanged = QtCore.pyqtSignal(int)
|
|
maxValueChanged = QtCore.pyqtSignal(int)
|
|
|
|
segmentsChanged = QtCore.pyqtSignal(list, int)
|
|
|
|
segments = []
|
|
last_update_segment = 0
|
|
|
|
def __init__(self, parent=None):
|
|
"""Create a new QRangeSlider instance.
|
|
|
|
:param parent: QWidget parent
|
|
:return: New QRangeSlider instance.
|
|
|
|
"""
|
|
super(QMultiRangeSlider, self).__init__(parent)
|
|
self.setMinimumSize(1, 30)
|
|
self.padding = 5
|
|
self.hande_width = 4
|
|
|
|
self.selected_segment = None
|
|
|
|
self.setMin(0)
|
|
self.setMax(99)
|
|
self.setRanges([(20, 30), [40, 90]])
|
|
|
|
def setMin(self, value):
|
|
"""sets minimum value"""
|
|
assert type(value) is int
|
|
setattr(self, '__min', value)
|
|
self.minValueChanged.emit(value)
|
|
self.segments = []
|
|
|
|
def setMax(self, value):
|
|
"""sets maximum value"""
|
|
assert type(value) is int
|
|
setattr(self, '__max', value)
|
|
self.maxValueChanged.emit(value)
|
|
self.segments = []
|
|
|
|
def initSegment(self):
|
|
self.segments = []
|
|
|
|
if (self.min() != None & self.max() != None):
|
|
self.setRanges([(self.min(), self.max())])
|
|
|
|
def keyPressEvent(self, event):
|
|
"""overrides key press event to move range left and right"""
|
|
key = event.key()
|
|
if key == QtCore.Qt.Key_Left:
|
|
s = self.start() - 1
|
|
e = self.end() - 1
|
|
elif key == QtCore.Qt.Key_Right:
|
|
s = self.start() + 1
|
|
e = self.end() + 1
|
|
else:
|
|
event.ignore()
|
|
return
|
|
event.accept()
|
|
if s >= self.parent().min() and e <= self.max():
|
|
self.setRange(s, e)
|
|
|
|
def min(self):
|
|
""":return: minimum value"""
|
|
return getattr(self, '__min', None)
|
|
|
|
def max(self):
|
|
""":return: maximum value"""
|
|
return getattr(self, '__max', None)
|
|
|
|
def _setStart(self, value):
|
|
"""stores the start value only"""
|
|
setattr(self, '__start', value)
|
|
self.startValueChanged.emit(value)
|
|
# self.segmentChanged.emit(value, self.end())
|
|
|
|
def setStart(self, values):
|
|
"""sets the range slider start value"""
|
|
# assert type(value) is int
|
|
|
|
for i, s in enumerate(self.segments):
|
|
s.setStart(values[i])
|
|
|
|
def setEnd(self, values):
|
|
"""set the range slider end value"""
|
|
# assert type(value) is int
|
|
|
|
for i, s in enumerate(self.segments):
|
|
s.setEnd(values[i])
|
|
|
|
def getRanges(self):
|
|
""":return: the start and end values as a tuple"""
|
|
ret = []
|
|
for i, s in enumerate(self.segments):
|
|
ret.append(s.getRange())
|
|
|
|
return ret
|
|
|
|
def setRanges(self, values):
|
|
"""set the start and end values"""
|
|
|
|
while len(self.segments) < len(values):
|
|
self.segments.append(QRangeSliderSegment(self))
|
|
|
|
while len(self.segments) > len(values):
|
|
self.segments.remove(-1)
|
|
|
|
for i, (s, e) in enumerate(values):
|
|
self.segments[i].setStart(s)
|
|
self.segments[i].setEnd(e)
|
|
|
|
self._trigger_refresh(0)
|
|
|
|
def mouseDoubleClickEvent(self, e):
|
|
self.selected_segment = None
|
|
|
|
d_width = self.width() - (self.padding * 2)
|
|
step_size = d_width / (self.max() - self.min())
|
|
pos = (e.x() - (self.padding)) / step_size
|
|
|
|
# removing existing segment
|
|
for (i, s) in enumerate(self.segments):
|
|
if (s.start() < pos) and (pos < s.end()):
|
|
# always keep one segment
|
|
if len(self.segments) > 1:
|
|
self.segments.remove(s)
|
|
# since one segement exists all the time the first can not be -1
|
|
if i == 0:
|
|
self._trigger_refresh(0)
|
|
else:
|
|
self._trigger_refresh(i - 1)
|
|
return
|
|
|
|
# if new segment in front
|
|
if (pos < self.segments[0].end()):
|
|
start = 0
|
|
end = self.segments[0].start()
|
|
diff = end - start
|
|
start += .25 * diff
|
|
end -= .25 * diff
|
|
start = int(start)
|
|
end = int(end)
|
|
seg = QRangeSliderSegment(self)
|
|
seg.setStart(start)
|
|
seg.setEnd(end)
|
|
self.segments.insert(0, seg)
|
|
self._trigger_refresh(0)
|
|
return
|
|
|
|
# if new segment in back
|
|
if (pos > self.segments[-1].end()):
|
|
start = self.segments[-1].end()
|
|
end = self.max()
|
|
diff = end - start
|
|
start += .25 * diff
|
|
end -= .25 * diff
|
|
start = int(start)
|
|
end = int(end)
|
|
seg = QRangeSliderSegment(self)
|
|
seg.setStart(start)
|
|
seg.setEnd(end)
|
|
self.segments.append(seg)
|
|
self._trigger_refresh(len(self.segments) - 1)
|
|
return
|
|
|
|
# if new segment in between
|
|
for i in range(len(self.segments) - 1):
|
|
if (self.segments[i].end() < pos) and (pos < self.segments[i + 1].start()):
|
|
start = self.segments[i].end()
|
|
end = self.segments[i + 1].start()
|
|
diff = end - start
|
|
start += .25 * diff
|
|
end -= .25 * diff
|
|
start = int(start)
|
|
end = int(end)
|
|
seg = QRangeSliderSegment(self)
|
|
seg.setStart(start)
|
|
seg.setEnd(end)
|
|
self.segments.insert(i + 1, seg)
|
|
self._trigger_refresh(i + 1)
|
|
return
|
|
|
|
def mousePressEvent(self, e):
|
|
# d_height = painter.device().height() - (self.padding * 2)
|
|
d_width = self.width() - (self.padding * 2)
|
|
step_size = d_width / (self.max() - self.min())
|
|
pos = (e.x() - (self.padding)) / step_size
|
|
|
|
distance = sys.maxsize
|
|
|
|
for i, s in enumerate(self.segments):
|
|
if (abs(s.start() - pos) < distance):
|
|
distance = abs(s.start() - pos)
|
|
if (distance * step_size < 10):
|
|
self.selected_segment = {"Seg": s, "Type": "start", "Index": i}
|
|
|
|
if (abs(s.end() - pos) < distance):
|
|
distance = abs(s.end() - pos)
|
|
if (distance * step_size < 10):
|
|
self.selected_segment = {"Seg": s, "Type": "end", "Index": i}
|
|
|
|
self._trigger_refresh(self.last_update_segment)
|
|
|
|
def mouseMoveEvent(self, e):
|
|
d_width = self.width() - (self.padding * 2)
|
|
step_size = d_width / (self.max() - self.min())
|
|
pos = int(round((e.x() - (self.padding)) / step_size))
|
|
|
|
if self.selected_segment is None:
|
|
return
|
|
|
|
s = self.selected_segment["Seg"]
|
|
if (self.selected_segment["Type"] == "start"):
|
|
if ((self.selected_segment["Index"] == 0) & (0 <= pos)):
|
|
s.setStart(pos)
|
|
elif (self.segments[self.selected_segment["Index"] - 1].end() < pos - 5) & (s.end() > pos + 5):
|
|
s.setStart(pos)
|
|
elif (self.selected_segment["Type"] == "end"):
|
|
if (self.selected_segment["Index"] == len(self.segments) - 1) & (pos <= self.max()):
|
|
s.setEnd(pos)
|
|
elif self.selected_segment["Index"] + 1 < len(self.segments) and (self.segments[self.selected_segment["Index"] + 1].start() > pos + 5) and (s.start() < pos - 5):
|
|
s.setEnd(pos)
|
|
|
|
self._trigger_refresh(self.selected_segment["Index"])
|
|
|
|
def mouseReleaseEvent(self, e):
|
|
self.selected_segment = None
|
|
|
|
def _trigger_refresh(self, last_update_segment):
|
|
self.last_update_segment = last_update_segment
|
|
self.update()
|
|
self.segmentsChanged.emit(self.getRanges(), last_update_segment)
|
|
|
|
def paintEvent(self, e):
|
|
painter = QtGui.QPainter(self)
|
|
brush = QtGui.QBrush()
|
|
brush.setColor(QtGui.QColor(53, 53, 53))
|
|
brush.setStyle(QtCore.Qt.SolidPattern)
|
|
rect = QtCore.QRect(0, 0, painter.device().width(), painter.device().height())
|
|
painter.fillRect(rect, brush)
|
|
|
|
# Define our canvas.
|
|
d_height = painter.device().height() - (self.padding * 2)
|
|
d_width = painter.device().width() - (self.padding * 2)
|
|
step_size = d_width / (self.max() - self.min())
|
|
|
|
for s in self.segments:
|
|
|
|
brush.setColor(QtGui.QColor('white'))
|
|
rect = QtCore.QRect(
|
|
self.padding + s.start() * step_size,
|
|
self.padding * 2,
|
|
(s.end() - s.start()) * step_size,
|
|
d_height - 2 * self.padding
|
|
)
|
|
painter.fillRect(rect, brush)
|
|
|
|
brush.setColor(QtGui.QColor('red'))
|
|
rect = QtCore.QRect(
|
|
self.padding + s.end() * step_size - (self.hande_width / 2),
|
|
self.padding,
|
|
(self.hande_width / 2),
|
|
d_height
|
|
)
|
|
painter.fillRect(rect, brush)
|
|
|
|
brush.setColor(QtGui.QColor('green'))
|
|
rect = QtCore.QRect(
|
|
self.padding + s.start() * step_size - (self.hande_width / 2),
|
|
self.padding,
|
|
(self.hande_width / 2),
|
|
d_height
|
|
)
|
|
painter.fillRect(rect, brush)
|
|
|
|
|
|
class QRangeSliderSegment(QtWidgets.QWidget):
|
|
|
|
# signals
|
|
# startValueChanged = QtCore.pyqtSignal(int)
|
|
# endValueChanged = QtCore.pyqtSignal(int)
|
|
# segmentChanged = QtCore.pyqtSignal(int, int)
|
|
|
|
def __init__(self, parent=None):
|
|
"""Create a new QRangeSlider instance.
|
|
|
|
:param parent: QWidget parent
|
|
:return: New QRangeSlider instance.
|
|
|
|
"""
|
|
super(QRangeSliderSegment, self).__init__(parent)
|
|
|
|
def start(self):
|
|
""":return: range slider start value"""
|
|
return getattr(self, '__start', None)
|
|
|
|
def end(self):
|
|
""":return: range slider end value"""
|
|
return getattr(self, '__end', None)
|
|
|
|
def getRange(self):
|
|
""":return: the start and end values as a tuple"""
|
|
return (self.start(), self.end())
|
|
|
|
def setRange(self, start, end):
|
|
"""set the start and end values"""
|
|
self.setStart(start)
|
|
self.setEnd(end)
|
|
|
|
def _setEnd(self, value):
|
|
"""stores the end value only"""
|
|
setattr(self, '__end', value)
|
|
# self.endValueChanged.emit(value)
|
|
# self.segmentChanged.emit(self.start(), value)
|
|
|
|
def setEnd(self, value):
|
|
"""set the range slider end value"""
|
|
assert type(value) is int
|
|
self._setEnd(value)
|
|
|
|
def _setStart(self, value):
|
|
"""stores the start value only"""
|
|
setattr(self, '__start', value)
|
|
# self.startValueChanged.emit(value)
|
|
# self.segmentChanged.emit(value, self.end())
|
|
|
|
def setStart(self, value):
|
|
"""sets the range slider start value"""
|
|
assert type(value) is int
|
|
self._setStart(value)
|