def on_tableview_selection_changed(self, selected: Qt.QItemSelection, deselected: Qt.QItemSelection) -> None: if len(selected.indexes()) == 0: self.delete_button.setEnabled(False) return index = selected.indexes()[0] scene = self.scening_list[index.row()] qt_silent_call(self.start_frame_control.setValue, scene.start) qt_silent_call(self. end_frame_control.setValue, scene.end) qt_silent_call(self. start_time_control.setValue, Time(scene.start)) qt_silent_call(self. end_time_control.setValue, Time(scene.end)) qt_silent_call(self. label_lineedit.setText, scene.label) self.delete_button.setEnabled(True)
def import_matroska_xml_chapters(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports chapters as scenes. Preserve end time and text if they're present. ''' from xml.etree import ElementTree timestamp_pattern = re.compile(r'(\d{2}):(\d{2}):(\d{2}(?:\.\d{3})?)') try: root = ElementTree.parse(str(path)).getroot() except ElementTree.ParseError as exc: logging.warning( f'Scening import: error occured' ' while parsing \'{path.name}\':') logging.warning(exc.msg) return for chapter in root.iter('ChapterAtom'): start_element = chapter.find('ChapterTimeStart') if start_element is None or start_element.text is None: continue match = timestamp_pattern.match(start_element.text) if match is None: continue start = Frame(Time( hours = int(match[1]), minutes = int(match[2]), seconds = float(match[3]) )) end = None end_element = chapter.find('ChapterTimeEnd') if end_element is not None and end_element.text is not None: match = timestamp_pattern.match(end_element.text) if match is not None: end = Frame(Time( hours = int(match[1]), minutes = int(match[2]), seconds = float(match[3]) )) label = '' label_element = chapter.find('ChapterDisplay/ChapterString') if label_element is not None and label_element.text is not None: label = label_element.text try: scening_list.add(start, end, label) except ValueError: out_of_range_count += 1
def import_ass(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports lines as scenes. Text is ignored. ''' import pysubs2 subs = pysubs2.load(str(path)) for line in subs: t_start = Time(milliseconds=line.start) t_end = Time(milliseconds=line.end) try: scening_list.add(Frame(t_start), Frame(t_end)) except ValueError: out_of_range_count += 1
def switch_frame(self, pos: Union[Frame, Time], *, render_frame: bool = True) -> None: if isinstance(pos, Frame): frame = pos time = Time(frame) elif isinstance(pos, Time): frame = Frame(pos) time = pos else: logging.debug('switch_frame(): position is neither Frame nor Time') return if frame > self.current_output.end_frame: return self.current_output.last_showed_frame = frame self.timeline.set_position(frame) self.toolbars.main.on_current_frame_changed(frame, time) for toolbar in self.toolbars: toolbar.on_current_frame_changed(frame, time) if render_frame: self.current_output.graphics_scene_item.setImage( self.render_frame(frame))
def switch_frame(self, frame: Optional[Frame] = None, time: Optional[Time] = None, *, render_frame: bool = True) -> None: if frame is not None: time = Time(frame) elif time is not None: frame = Frame(time) else: logging.debug('switch_frame(): both frame and time is None') return if frame > self.current_output.end_frame: return self.current_output.last_showed_frame = frame self.timeline.set_position(frame) self.toolbars.main.on_current_frame_changed(frame, time) for toolbar in self.toolbars: toolbar.on_current_frame_changed(frame, time) if render_frame: self.current_output.graphics_scene_item.setImage( self.render_frame(frame))
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)
def data(self, index: Qt.QModelIndex, role: int = Qt.Qt.UserRole) -> Any: if not index.isValid(): return None row = index.row() if row >= len(self.items): return None column = index.column() if column >= self.COLUMN_COUNT: return None if role in (Qt.Qt.DisplayRole, Qt.Qt.EditRole): if column == self.START_FRAME_COLUMN: return str(self.items[row].start) if column == self.END_FRAME_COLUMN: if self.items[row].end != self.items[row].start: return str(self.items[row].end) else: return '' if column == self.START_TIME_COLUMN: return str(Time(self.items[row].start)) if column == self.END_TIME_COLUMN: if self.items[row].end != self.items[row].start: return str(Time(self.items[row].end)) else: return '' if column == self.LABEL_COLUMN: return str(self.items[row].label) if role == Qt.Qt.UserRole: if column == self.START_FRAME_COLUMN: return self.items[row].start if column == self.END_FRAME_COLUMN: return self.items[row].end if column == self.START_TIME_COLUMN: return Time(self.items[row].start) if column == self.END_TIME_COLUMN: return Time(self.items[row].end) if column == self.LABEL_COLUMN: return self.items[row].label return None
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))
def import_matroska_timestamps_v2(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports intervals of constant FPS as scenes. Uses FPS for scene label. ''' timestamps: List[Time] = [] for line in path.read_text().splitlines(): try: timestamps.append(Time(milliseconds=float(line))) except ValueError: continue if len(timestamps) < 2: logging.warning( 'Scening import: timestamps file contains less than' ' 2 timestamps, so there\'s nothing to import.') return deltas = [ timestamps[i] - timestamps[i - 1] for i in range(1, len(timestamps)) ] scene_delta = deltas[0] scene_start = Frame(0) scene_end: Optional[Frame] = None for i in range(1, len(deltas)): if abs(round(float(deltas[i] - scene_delta), 6)) <= 0.000_001: continue # TODO: investigate, why offset by -1 is necessary here scene_end = Frame(i - 1) try: scening_list.add( scene_start, scene_end, '{:.3f} fps'.format(1 / float(scene_delta))) except ValueError: out_of_range_count += 1 scene_start = Frame(i) scene_end = None scene_delta = deltas[i] if scene_end is None: try: scening_list.add( scene_start, Frame(len(timestamps) - 1), '{:.3f} fps'.format(1 / float(scene_delta))) except ValueError: out_of_range_count += 1
def import_ogm_chapters(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports chapters as signle-frame scenes. Uses NAME for scene label. ''' pattern = re.compile( r'(CHAPTER\d+)=(\d{2}):(\d{2}):(\d{2}(?:\.\d{3})?)\n\1NAME=(.*)', re.RegexFlag.MULTILINE) for match in pattern.finditer(path.read_text()): time = Time( hours = int(match[2]), minutes = int(match[3]), seconds = float(match[4])) try: scening_list.add(Frame(time), label=match[5]) except ValueError: out_of_range_count += 1
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 set_end_frame(self, end_f: Frame) -> None: self.end_f = end_f self.end_t = Time(end_f) self.full_repaint()
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 current_time(self) -> Time: # type: ignore return Time(self.current_output.last_showed_frame)
''' Imports listed scenes, ignoring gaps. Uses FPS for scene label. ''' 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