conan/uiwidget/widgettimeline.py

452 lines
15 KiB
Python
Raw Normal View History

2021-10-17 12:49:49 +02:00
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)