def generate_label_format(self, notch_interval_t: TimeInterval) -> str: if notch_interval_t >= TimeInterval(hours=1): return '%h:%M' elif notch_interval_t >= TimeInterval(minutes=1): return '%m:00' else: return '%m:%S'
def on_current_output_changed(self, index: int, prev_index: int) -> None: qt_silent_call(self.seek_frame_control.setMaximum, self.main.current_output.total_frames) qt_silent_call(self.seek_time_control.setMaximum, self.main.current_output.total_time) qt_silent_call(self.seek_time_control.setMinimum, TimeInterval(FrameInterval(1))) qt_silent_call(self.seek_time_control.setValue, TimeInterval(self.seek_frame_control.value())) qt_silent_call(self.fps_spinbox.setValue, self.main.current_output.play_fps)
def update_statusbar_output_info(self, output: Optional[Output] = None) -> None: if output is None: output = self.current_output self.statusbar.total_frames_label.setText('{} frames'.format( output.total_frames)) self.statusbar.duration_label.setText( # Display duration without -1 offset to match other video tools '{}'.format(TimeInterval(self.current_output.total_frames))) self.statusbar.resolution_label.setText('{}x{}'.format( output.width, output.height)) if not output.has_alpha: self.statusbar.pixel_format_label.setText('{}'.format( output.format.name)) else: self.statusbar.pixel_format_label.setText( 'Clip: {}, Alpha: {}'.format(output.format.name, output.format_alpha.name)) if output.fps_den != 0: self.statusbar.fps_label.setText('{}/{} = {:.3f} fps'.format( output.fps_num, output.fps_den, output.fps_num / output.fps_den)) else: self.statusbar.fps_label.setText('{}/{} fps'.format( output.fps_num, output.fps_den))
def update_info(self) -> None: run_time = TimeInterval(seconds=(perf_counter() - self.run_start_time)) frames_done = self.total_frames - self.frames_left fps = int(frames_done) / float(run_time) info_str = ("{}/{} frames in {}, {:.3f} fps".format( frames_done, self.total_frames, run_time, fps)) self.info_label.setText(info_str)
def on_current_output_changed(self, index: int, prev_index: int) -> None: self. start_frame_control.setMaximum(self.main.current_output.end_frame) self. start_time_control.setMaximum(self.main.current_output.end_time) self. end_frame_control.setMaximum(self.main.current_output.end_frame) self. end_time_control.setMaximum(self.main.current_output.end_time) self.total_frames_control.setMaximum(self.main.current_output.total_frames) self. total_time_control.setMaximum(self.main.current_output.total_time) self. total_time_control.setMaximum(TimeInterval(FrameInterval(1)))
def import_cue(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports tracks as scenes. Uses TITLE for scene label. ''' from cueparser import CueSheet def offset_to_time(offset: str) -> Optional[Time]: pattern = re.compile(r'(\d{1,2}):(\d{1,2}):(\d{1,2})') match = pattern.match(offset) if match is None: return None return Time( minutes = int(match[1]), seconds = int(match[2]), milliseconds = int(match[3]) / 75 * 1000) cue_sheet = CueSheet() cue_sheet.setOutputFormat('') cue_sheet.setData(path.read_text()) cue_sheet.parse() for track in cue_sheet.tracks: if track.offset is None: continue offset = offset_to_time(track.offset) if offset is None: logging.warning( f'Scening import: INDEX timestamp \'{track.offset}\'' ' format isn\'t suported.') continue start = Frame(offset) end = None if track.duration is not None: end = Frame(offset + TimeInterval(track.duration)) label = '' if track.title is not None: label = track.title try: scening_list.add(start, end, label) except ValueError: out_of_range_count += 1
def update_controls(self, start: Optional[Frame] = None, end: Optional[Frame] = None, total: Optional[FrameInterval] = None) -> None: if start is not None: end = self.end_frame_control.value() total = self.total_frames_control.value() if start > end: end = start total = end - start + FrameInterval(1) elif end is not None: start = self.start_frame_control.value() total = self.total_frames_control.value() if end < start: start = end total = end - start + FrameInterval(1) elif total is not None: start = self.start_frame_control.value() end = self.end_frame_control.value() old_total = end - start + FrameInterval(1) delta = total - old_total end += delta if end > self.main.current_output.end_frame: start -= end - self.main.current_output.end_frame end = self.main.current_output.end_frame else: return qt_silent_call(self.start_frame_control.setValue, start) qt_silent_call(self.start_time_control.setValue, Time(start)) qt_silent_call(self.end_frame_control.setValue, end) qt_silent_call(self.end_time_control.setValue, Time(end)) qt_silent_call(self.total_frames_control.setValue, total) qt_silent_call(self.total_time_control.setValue, TimeInterval(total))
class Timeline(Qt.QWidget): __slots__ = ( 'app', 'main', 'rectF', 'prevRectF', 'totalT', 'totalF', 'notchIntervalTargetX', 'notchHeight', 'fontHeight', 'notchLabelInterval', 'notchScrollInterval', 'scrollHeight', 'cursorX', 'cursorFT', 'needFullRepaint', 'scrollRect', ) class Mode(YAMLObject): yaml_tag = '!Timeline.Mode' FRAME = 'frame' TIME = 'time' @classmethod def is_valid(cls, value: str) -> bool: return value in (cls.FRAME, cls.TIME) clicked = Qt.pyqtSignal(Frame, Time) def __init__(self, parent: Qt.QWidget) -> None: from vspreview.utils import main_window super().__init__(parent) self.app = Qt.QApplication.instance() self.main = main_window() self._mode = self.Mode.TIME self.rect_f = Qt.QRectF() self.end_t = Time(seconds=1) self.end_f = Frame(1) self.notch_interval_target_x = round(75 * self.main.display_scale) self.notch_height = round(6 * self.main.display_scale) self.font_height = round(10 * self.main.display_scale) self.notch_label_interval = round(-1 * self.main.display_scale) self.notch_scroll_interval = round(2 * self.main.display_scale) self.scroll_height = round(10 * self.main.display_scale) self.setMinimumSize(self.notch_interval_target_x, round(33 * self.main.display_scale)) font = self.font() font.setPixelSize(self.font_height) self.setFont(font) self.cursor_x = 0 # used as a fallback when self.rectF.width() is 0, # so cursorX is incorrect self.cursor_ftx: Optional[Union[Frame, Time, int]] = None # False means that only cursor position'll be recalculated self.need_full_repaint = True self.toolbars_notches: Dict[AbstractToolbar, Notches] = {} self.setAttribute(Qt.Qt.WA_OpaquePaintEvent) self.setMouseTracking(True) def paintEvent(self, event: Qt.QPaintEvent) -> None: super().paintEvent(event) self.rect_f = Qt.QRectF(event.rect()) # self.rectF.adjust(0, 0, -1, -1) if self.cursor_ftx is not None: self.set_position(self.cursor_ftx) self.cursor_ftx = None painter = Qt.QPainter(self) self.drawWidget(painter) def drawWidget(self, painter: Qt.QPainter) -> None: from copy import deepcopy from vspreview.utils import strfdelta # calculations if self.need_full_repaint: labels_notches = Notches() label_notch_bottom = (self.rect_f.top() + self.font_height + self.notch_label_interval + self.notch_height + 5) label_notch_top = label_notch_bottom - self.notch_height label_notch_x = self.rect_f.left() if self.mode == self.Mode.TIME: notch_interval_t = self.calculate_notch_interval_t( self.notch_interval_target_x) label_format = self.generate_label_format( notch_interval_t, self.end_t) label_notch_t = Time() while (label_notch_x < self.rect_f.right() and label_notch_t <= self.end_t): line = Qt.QLineF(label_notch_x, label_notch_bottom, label_notch_x, label_notch_top) labels_notches.add( Notch(deepcopy(label_notch_t), line=line)) label_notch_t += notch_interval_t label_notch_x = self.t_to_x(label_notch_t) elif self.mode == self.Mode.FRAME: notch_interval_f = self.calculate_notch_interval_f( self.notch_interval_target_x) label_notch_f = Frame(0) while (label_notch_x < self.rect_f.right() and label_notch_f <= self.end_f): line = Qt.QLineF(label_notch_x, label_notch_bottom, label_notch_x, label_notch_top) labels_notches.add( Notch(deepcopy(label_notch_f), line=line)) label_notch_f += notch_interval_f label_notch_x = self.f_to_x(label_notch_f) self.scroll_rect = Qt.QRectF( self.rect_f.left(), label_notch_bottom + self.notch_scroll_interval, self.rect_f.width(), self.scroll_height) for toolbar, notches in self.toolbars_notches.items(): if not toolbar.is_notches_visible(): continue for notch in notches: if isinstance(notch.data, Frame): x = self.f_to_x(notch.data) elif isinstance(notch.data, Time): x = self.t_to_x(notch.data) y = self.scroll_rect.top() notch.line = Qt.QLineF(x, y, x, y + self.scroll_rect.height() - 1) cursor_line = Qt.QLineF( self.cursor_x, self.scroll_rect.top(), self.cursor_x, self.scroll_rect.top() + self.scroll_rect.height() - 1) # drawing if self.need_full_repaint: painter.fillRect(self.rect_f, self.palette().color(Qt.QPalette.Window)) painter.setPen( Qt.QPen(self.palette().color(Qt.QPalette.WindowText))) painter.setRenderHint(Qt.QPainter.Antialiasing, False) painter.drawLines([notch.line for notch in labels_notches]) painter.setRenderHint(Qt.QPainter.Antialiasing) for i, notch in enumerate(labels_notches): line = notch.line anchor_rect = Qt.QRectF(line.x2(), line.y2() - self.notch_label_interval, 0, 0) if self.mode == self.Mode.TIME: time = cast(Time, notch.data) label = strfdelta(time, label_format) if self.mode == self.Mode.FRAME: label = str(notch.data) if i == 0: rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignLeft, label) if self.mode == self.Mode.TIME: rect.moveLeft(-2.5) elif i == (len(labels_notches) - 1): rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignHCenter, label) if rect.right() > self.rect_f.right(): rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignRight, label) else: rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignHCenter, label) painter.drawText(rect, label) painter.setRenderHint(Qt.QPainter.Antialiasing, False) painter.fillRect(self.scroll_rect, Qt.Qt.gray) for toolbar, notches in self.toolbars_notches.items(): if not toolbar.is_notches_visible(): continue for notch in notches: painter.setPen(notch.color) painter.drawLine(notch.line) painter.setPen(Qt.Qt.black) painter.drawLine(cursor_line) self.need_full_repaint = False def full_repaint(self) -> None: self.need_full_repaint = True self.update() def moveEvent(self, event: Qt.QMoveEvent) -> None: super().moveEvent(event) self.full_repaint() def mousePressEvent(self, event: Qt.QMouseEvent) -> None: super().mousePressEvent(event) pos = Qt.QPoint(event.pos()) if self.scroll_rect.contains(pos): self.set_position(pos.x()) self.clicked.emit(self.x_to_f(self.cursor_x, Frame), self.x_to_t(self.cursor_x, Time)) def mouseMoveEvent(self, event: Qt.QMouseEvent) -> None: super().mouseMoveEvent(event) for toolbar, notches in self.toolbars_notches.items(): if not toolbar.is_notches_visible(): continue for notch in notches: line = notch.line if line.x1() - 0.5 <= event.x() <= line.x1() + 0.5: Qt.QToolTip.showText(event.globalPos(), notch.label) return def resizeEvent(self, event: Qt.QResizeEvent) -> None: super().resizeEvent(event) self.full_repaint() def event(self, event: Qt.QEvent) -> bool: if event.type() in (Qt.QEvent.Polish, Qt.QEvent.ApplicationPaletteChange): self.setPalette(self.main.palette()) self.full_repaint() return True return super().event(event) def update_notches(self, toolbar: Optional[AbstractToolbar] = None) -> None: if toolbar is not None: self.toolbars_notches[toolbar] = toolbar.get_notches() if toolbar is None: for t in self.main.toolbars: self.toolbars_notches[t] = t.get_notches() self.full_repaint() @property def mode(self) -> str: # pylint: disable=undefined-variable return self._mode @mode.setter def mode(self, value: str) -> None: if value == self._mode: return self._mode = value self.full_repaint() notch_intervals_t = ( TimeInterval(seconds=1), TimeInterval(seconds=2), TimeInterval(seconds=5), TimeInterval(seconds=10), TimeInterval(seconds=15), TimeInterval(seconds=30), TimeInterval(seconds=60), TimeInterval(seconds=90), TimeInterval(seconds=120), TimeInterval(seconds=300), TimeInterval(seconds=600), TimeInterval(seconds=900), TimeInterval(seconds=1200), TimeInterval(seconds=1800), TimeInterval(seconds=2700), TimeInterval(seconds=3600), TimeInterval(seconds=5400), TimeInterval(seconds=7200), ) def calculate_notch_interval_t(self, target_interval_x: int) -> TimeInterval: margin = 1 + self.main.TIMELINE_LABEL_NOTCHES_MARGIN / 100 target_interval_t = self.x_to_t(target_interval_x, TimeInterval) if target_interval_t >= self.notch_intervals_t[-1] * margin: return self.notch_intervals_t[-1] for interval in self.notch_intervals_t: if target_interval_t < interval * margin: return interval raise RuntimeError notch_intervals_f = ( FrameInterval(1), FrameInterval(5), FrameInterval(10), FrameInterval(20), FrameInterval(25), FrameInterval(50), FrameInterval(75), FrameInterval(100), FrameInterval(200), FrameInterval(250), FrameInterval(500), FrameInterval(750), FrameInterval(1000), FrameInterval(2000), FrameInterval(2500), FrameInterval(5000), FrameInterval(7500), FrameInterval(10000), FrameInterval(20000), FrameInterval(25000), FrameInterval(50000), FrameInterval(75000), ) def calculate_notch_interval_f(self, target_interval_x: int) -> FrameInterval: margin = 1 + self.main.TIMELINE_LABEL_NOTCHES_MARGIN / 100 target_interval_f = self.x_to_f(target_interval_x, FrameInterval) if target_interval_f >= FrameInterval( round(int(self.notch_intervals_f[-1]) * margin)): return self.notch_intervals_f[-1] for interval in self.notch_intervals_f: if target_interval_f < FrameInterval(round( int(interval) * margin)): return interval raise RuntimeError def generate_label_format(self, notch_interval_t: TimeInterval, end_time: TimeInterval) -> str: if end_time >= TimeInterval(hours=1): return '%h:%M:00' elif notch_interval_t >= TimeInterval(minutes=1): return '%m:00' else: return '%m:%S' def set_end_frame(self, end_f: Frame) -> None: self.end_f = end_f self.end_t = Time(end_f) self.full_repaint() def set_position(self, pos: Union[Frame, Time, int]) -> None: if self.rect_f.width() == 0.0: self.cursor_ftx = pos if isinstance(pos, Frame): self.cursor_x = self.f_to_x(pos) elif isinstance(pos, Time): self.cursor_x = self.t_to_x(pos) elif isinstance(pos, int): self.cursor_x = pos else: raise TypeError self.update() def t_to_x(self, t: TimeType) -> int: width = self.rect_f.width() try: x = round(float(t) / float(self.end_t) * width) except ZeroDivisionError: x = 0 return x def x_to_t(self, x: int, ty: Type[TimeType]) -> TimeType: width = self.rect_f.width() return ty(seconds=(x * float(self.end_t) / width)) def f_to_x(self, f: FrameType) -> int: width = self.rect_f.width() try: x = round(int(f) / int(self.end_f) * width) except ZeroDivisionError: x = 0 return x def x_to_f(self, x: int, ty: Type[FrameType]) -> FrameType: width = self.rect_f.width() value = round(x / width * int(self.end_f)) return ty(value)
def on_seek_frame_changed(self, frame: FrameInterval) -> None: qt_silent_call(self.seek_time_control.setValue, TimeInterval(frame))
def drawWidget(self, painter: Qt.QPainter) -> None: from copy import deepcopy from vspreview.utils import strfdelta # calculations if self.need_full_repaint: labels_notches = Notches() label_notch_bottom = (self.rect_f.top() + self.font_height + self.notch_label_interval + self.notch_height + 5) label_notch_top = label_notch_bottom - self.notch_height label_notch_x = self.rect_f.left() if self.mode == self.Mode.TIME: notch_interval_t = self.calculate_notch_interval_t( self.notch_interval_target_x) label_format = self.generate_label_format( notch_interval_t, TimeInterval(self.end_t.value)) label_notch_t = Time() while (label_notch_x < self.rect_f.right() and label_notch_t <= self.end_t): line = Qt.QLineF(label_notch_x, label_notch_bottom, label_notch_x, label_notch_top) labels_notches.add( Notch(deepcopy(label_notch_t), line=line)) label_notch_t += notch_interval_t label_notch_x = self.t_to_x(label_notch_t) elif self.mode == self.Mode.FRAME: notch_interval_f = self.calculate_notch_interval_f( self.notch_interval_target_x) label_notch_f = Frame(0) while (label_notch_x < self.rect_f.right() and label_notch_f <= self.end_f): line = Qt.QLineF(label_notch_x, label_notch_bottom, label_notch_x, label_notch_top) labels_notches.add( Notch(deepcopy(label_notch_f), line=line)) label_notch_f += notch_interval_f label_notch_x = self.f_to_x(label_notch_f) self.scroll_rect = Qt.QRectF( self.rect_f.left(), label_notch_bottom + self.notch_scroll_interval, self.rect_f.width(), self.scroll_height) for toolbar, notches in self.toolbars_notches.items(): if not toolbar.is_notches_visible(): continue for notch in notches: if isinstance(notch.data, Frame): x = self.f_to_x(notch.data) elif isinstance(notch.data, Time): x = self.t_to_x(notch.data) y = self.scroll_rect.top() notch.line = Qt.QLineF(x, y, x, y + self.scroll_rect.height() - 1) cursor_line = Qt.QLineF( self.cursor_x, self.scroll_rect.top(), self.cursor_x, self.scroll_rect.top() + self.scroll_rect.height() - 1) # drawing if self.need_full_repaint: painter.fillRect(self.rect_f, self.palette().color(Qt.QPalette.Window)) painter.setPen( Qt.QPen(self.palette().color(Qt.QPalette.WindowText))) painter.setRenderHint(Qt.QPainter.Antialiasing, False) painter.drawLines([notch.line for notch in labels_notches]) painter.setRenderHint(Qt.QPainter.Antialiasing) for i, notch in enumerate(labels_notches): line = notch.line anchor_rect = Qt.QRectF(line.x2(), line.y2() - self.notch_label_interval, 0, 0) if self.mode == self.Mode.TIME: time = cast(Time, notch.data) label = strfdelta(time, label_format) if self.mode == self.Mode.FRAME: label = str(notch.data) if i == 0: rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignLeft, label) if self.mode == self.Mode.TIME: rect.moveLeft(-2.5) elif i == (len(labels_notches) - 1): rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignHCenter, label) if rect.right() > self.rect_f.right(): rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignRight, label) else: rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignHCenter, label) painter.drawText(rect, label) painter.setRenderHint(Qt.QPainter.Antialiasing, False) painter.fillRect(self.scroll_rect, Qt.Qt.gray) for toolbar, notches in self.toolbars_notches.items(): if not toolbar.is_notches_visible(): continue for notch in notches: painter.setPen(notch.color) painter.drawLine(notch.line) painter.setPen(Qt.Qt.black) painter.drawLine(cursor_line) self.need_full_repaint = False
''' pattern = re.compile( r'^((?:\d+(?:\.\d+)?)|gap)(?:,\s?(\d+(?:\.\d+)?))?', re.RegexFlag.MULTILINE) assume_pattern = re.compile(r'assume (\d+(?:\.\d+))') if len(match := assume_pattern.findall(path.read_text())) > 0: default_fps = float(match[0]) else: logging.warning('Scening import: "assume" entry not found.') return pos = Time() for match in pattern.finditer(path.read_text()): if match[1] == 'gap': pos += TimeInterval(seconds=float(match[2])) continue interval = TimeInterval(seconds=float(match[1])) fps = float(match[2]) if match.lastindex >= 2 else default_fps scening_list.add( self.main.current_output.to_frame(pos), self.main.current_output.to_frame(pos + interval), '{:.3f} fps'.format(fps)) pos += interval def import_tfm(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports TFM's 'OVR HELP INFORMATION'.