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)