conan/uiwidget/widgettimeline.py
2021-10-17 12:49:49 +02:00

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)