def import_lwi(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None:
        '''
        Imports Key=1 frames as single-frame scenes.
        Ignores everything besides Index=0 video stream.
        '''
        from copy import deepcopy

        AV_CODEC_ID_FIRST_AUDIO = 0x10000
        STREAM_INDEX = 0
        IS_KEY = 1

        pattern = re.compile(r'Index={}.*?Codec=(\d+).*?\n.*?Key=(\d)'.format(
            STREAM_INDEX
        ))

        frame = Frame(0)
        for match in pattern.finditer(path.read_text(),
                                      re.RegexFlag.MULTILINE):
            if int(match[1]) >= AV_CODEC_ID_FIRST_AUDIO:
                frame += FrameInterval(1)
                continue

            if not int(match[2]) == IS_KEY:
                frame += FrameInterval(1)
                continue

            try:
                scening_list.add(deepcopy(frame))
            except ValueError:
                out_of_range_count += 1

            frame += FrameInterval(1)
Esempio n. 2
0
    def __init__(self, main: AbstractMainWindow) -> None:
        super().__init__(main, 'Benchmark')

        self.setup_ui()

        self.running = False
        self.unsequenced = False
        self.buffer: Deque[Future] = deque()
        self.run_start_time = 0.0
        self.start_frame  = Frame(0)
        self.  end_frame  = Frame(0)
        self.total_frames = FrameInterval(0)
        self.frames_left  = FrameInterval(0)

        self.sequenced_timer = Qt.QTimer()
        self.sequenced_timer.setTimerType(Qt.Qt.PreciseTimer)
        self.sequenced_timer.setInterval(0)

        self.update_info_timer = Qt.QTimer()
        self.update_info_timer.setTimerType(Qt.Qt.PreciseTimer)
        self.update_info_timer.setInterval(
            self.main.BENCHMARK_REFRESH_INTERVAL)

        self. start_frame_control.valueChanged.connect(lambda value: self.update_controls(start=      value))
        self.  start_time_control.valueChanged.connect(lambda value: self.update_controls(start=Frame(value)))
        self.   end_frame_control.valueChanged.connect(lambda value: self.update_controls(  end=      value))
        self.    end_time_control.valueChanged.connect(lambda value: self.update_controls(  end=Frame(value)))
        self.total_frames_control.valueChanged.connect(lambda value: self.update_controls(total=              value))
        self.  total_time_control.valueChanged.connect(lambda value: self.update_controls(total=FrameInterval(value)))
        self.   prefetch_checkbox.stateChanged.connect(self.on_prefetch_changed)
        self.    run_abort_button.     clicked.connect(self.on_run_abort_pressed)
        self.     sequenced_timer.     timeout.connect(self._request_next_frame_sequenced)
        self.   update_info_timer.     timeout.connect(self.update_info)

        set_qobject_names(self)
 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
Esempio n. 4
0
    def get_prev_frame(self, initial: Frame) -> Optional[Frame]:
        result = None
        result_delta = FrameInterval(int(self.main.current_output.end_frame))
        for scene in self.items:
            if FrameInterval(0) < initial - scene.start < result_delta:
                result = scene.start
                result_delta = initial - scene.start
            if FrameInterval(0) < initial - scene.end < result_delta:
                result = scene.end
                result_delta = initial - scene.end

        return result
Esempio n. 5
0
    def get_next_frame(self, initial: Frame) -> Optional[Frame]:
        result = None
        result_delta = FrameInterval(int(self.main.current_output.end_frame))
        for scene in self.items:
            if FrameInterval(0) < scene.start - initial < result_delta:
                result = scene.start
                result_delta = scene.start - initial
            if FrameInterval(0) < scene.end - initial < result_delta:
                result = scene.end
                result_delta = scene.end - initial

        return result
Esempio n. 6
0
    def _request_next_frame_sequenced(self) -> None:
        if self.frames_left <= FrameInterval(0):
            self.abort()
            return

        self.buffer.pop().result()

        next_frame = self.end_frame + FrameInterval(1) - self.frames_left
        if next_frame <= self.end_frame:
            new_future = self.main.current_output.vs_output.get_frame_async(
                int(next_frame))
            self.buffer.appendleft(new_future)

        self.frames_left -= FrameInterval(1)
Esempio n. 7
0
    def _request_next_frame_unsequenced(self, future: Optional[Future] = None) -> None:
        if self.frames_left <= FrameInterval(0):
            self.abort()
            return

        if self.running:
            next_frame = self.end_frame + FrameInterval(1) - self.frames_left
            new_future = self.main.current_output.vs_output.get_frame_async(
                int(next_frame))
            new_future.add_done_callback(self._request_next_frame_unsequenced)

        if future is not None:
            future.result()
        self.frames_left -= FrameInterval(1)
Esempio n. 8
0
    def play(self) -> None:
        if self.main.current_frame == self.main.current_output.end_frame:
            return

        if self.main.statusbar.label.text() == 'Ready':
            self.main.statusbar.label.setText('Playing')

        if not self.main.current_output.has_alpha:
            play_buffer_size = int(
                min(
                    self.main.PLAY_BUFFER_SIZE,
                    self.main.current_output.end_frame -
                    self.main.current_frame))
            self.play_buffer = deque([], play_buffer_size)
            for i in range(cast(int, self.play_buffer.maxlen)):
                future = self.main.current_output.vs_output.get_frame_async(
                    int(self.main.current_frame + FrameInterval(i) +
                        FrameInterval(1)))
                self.play_buffer.appendleft(future)
        else:
            play_buffer_size = int(
                min(self.main.PLAY_BUFFER_SIZE,
                    (self.main.current_output.end_frame -
                     self.main.current_frame) * 2))
            # buffer size needs to be even in case alpha is present
            play_buffer_size -= play_buffer_size % 2
            self.play_buffer = deque([], play_buffer_size)

            for i in range(cast(int, self.play_buffer.maxlen) // 2):
                frame = (self.main.current_frame + FrameInterval(i) +
                         FrameInterval(1))
                future = self.main.current_output.vs_output.get_frame_async(
                    int(frame))
                self.play_buffer.appendleft(future)
                future = self.main.current_output.vs_alpha.get_frame_async(
                    int(frame))
                self.play_buffer.appendleft(future)

        if self.fps_unlimited_checkbox.isChecked() or self.main.DEBUG_PLAY_FPS:
            self.play_timer.start(0)
            if self.main.DEBUG_PLAY_FPS:
                self.play_start_time = debug.perf_counter_ns()
                self.play_start_frame = self.main.current_frame
            else:
                self.fps_timer.start(self.main.FPS_REFRESH_INTERVAL)
        else:
            self.play_timer.start(
                round(1000 / self.main.current_output.play_fps))
Esempio n. 9
0
 def on_current_output_changed(self, index: int, prev_index: int) -> None:
     qt_silent_call(self.outputs_combobox.setCurrentIndex, index)
     qt_silent_call(
         self.frame_spinbox.setMaximum,
         self.main.current_output.total_frames - FrameInterval(1))
     qt_silent_call(self.time_spinbox.setMaximumTime,
                    timedelta_to_qtime(self.main.current_output.duration))
Esempio n. 10
0
 def seek_n_frames_f(self, checked: Optional[bool] = None) -> None:
     new_pos = self.main.current_frame + FrameInterval(
         self.seek_frame_spinbox.value())
     if new_pos >= self.main.current_output.total_frames:
         return
     self.stop()
     self.main.current_frame = new_pos
Esempio n. 11
0
 def seek_n_frames_f(self, checked: Optional[bool] = None) -> None:
     new_pos = (self.main.current_frame +
                FrameInterval(self.seek_frame_control.value()))
     if new_pos > self.main.current_output.end_frame:
         return
     self.stop()
     self.main.current_frame = new_pos
Esempio n. 12
0
 def seek_to_prev(self, checked: Optional[bool] = None) -> None:
     try:
         new_pos = self.main.current_frame - FrameInterval(1)
     except ValueError:
         return
     self.stop()
     self.main.current_frame = new_pos
Esempio n. 13
0
 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)))
Esempio n. 14
0
 def on_current_output_changed(self, index: int, prev_index: int) -> None:
     qt_silent_call(
         self.seek_frame_spinbox.setMaximum,
         self.main.current_output.total_frames - FrameInterval(1))
     qt_silent_call(self.seek_time_spinbox.setMaximumTime,
                    timedelta_to_qtime(self.main.current_output.duration))
     qt_silent_call(self.fps_spinbox.setValue,
                    self.main.current_output.play_fps)
Esempio n. 15
0
    def _show_next_frame(self) -> None:
        if not self.main.current_output.has_alpha:
            try:
                frame_future = self.play_buffer.pop()
            except IndexError:
                self.play_pause_button.click()
                return

            next_frame_for_buffer = (self.main.current_frame +
                                     self.main.PLAY_BUFFER_SIZE)
            if next_frame_for_buffer <= self.main.current_output.end_frame:
                self.play_buffer.appendleft(
                    self.main.current_output.vs_output.get_frame_async(
                        next_frame_for_buffer))

            self.main.switch_frame(self.main.current_frame + FrameInterval(1),
                                   render_frame=False)
            image = self.main.current_output.render_raw_videoframe(
                frame_future.result())
        else:
            try:
                frame_future = self.play_buffer.pop()
                alpha_future = self.play_buffer.pop()
            except IndexError:
                self.play_pause_button.click()
                return

            next_frame_for_buffer = (self.main.current_frame +
                                     self.main.PLAY_BUFFER_SIZE // 2)
            if next_frame_for_buffer <= self.main.current_output.end_frame:
                self.play_buffer.appendleft(
                    self.main.current_output.vs_output.get_frame_async(
                        next_frame_for_buffer))
                self.play_buffer.appendleft(
                    self.main.current_output.vs_alpha.get_frame_async(
                        next_frame_for_buffer))

            self.main.switch_frame(self.main.current_frame + FrameInterval(1),
                                   render_frame=False)
            image = self.main.current_output.render_raw_videoframe(
                frame_future.result(), alpha_future.result())

        self.main.current_output.graphics_scene_item.setImage(image)

        if not self.main.DEBUG_PLAY_FPS:
            self.update_fps_counter()
Esempio n. 16
0
 def seek_n_frames_b(self, checked: Optional[bool] = None) -> None:
     try:
         new_pos = (self.main.current_frame -
                    FrameInterval(self.seek_frame_control.value()))
     except ValueError:
         return
     self.stop()
     self.main.current_frame = new_pos
Esempio n. 17
0
 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.fps_spinbox.setValue,
                    self.main.current_output.play_fps)
Esempio n. 18
0
    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))
Esempio n. 19
0
    def play(self) -> None:
        if self.main.current_frame == self.main.current_output.total_frames - FrameInterval(
                1):
            return

        if self.main.statusbar.label.text() == 'Ready':
            self.main.statusbar.label.setText('Playing')

        self.fps_prev_frame = self.main.current_frame
        self.fps_timer.start(self.main.FPS_REFRESH_INTERVAL)

        self.play_buffer.clear()
        for i in range(self.main.PLAY_BUFFER_SIZE):
            future = self.main.current_output.vs_output.get_frame_async(
                int(self.main.current_frame + FrameInterval(i) +
                    FrameInterval(1)))
            self.play_buffer.appendleft(future)

        if self.fps_unlimited_checkbox.isChecked():
            self.play_timer.start(0)
        else:
            self.play_timer.start(
                round(1000 / self.main.current_output.play_fps))
Esempio n. 20
0
    def _playback_show_next_frame(self) -> None:
        try:
            frame_future = self.play_buffer.pop()
        except IndexError:
            self.play_pause_button.click()
            return

        self.main.on_current_frame_changed(self.main.current_frame +
                                           FrameInterval(1),
                                           render_frame=False)
        pixmap = self.main.render_raw_videoframe(frame_future.result())
        self.main.current_output.graphics_scene_item.setPixmap(pixmap)

        next_frame_for_buffer = self.main.current_frame + self.main.PLAY_BUFFER_SIZE
        if next_frame_for_buffer < self.main.current_output.total_frames:
            self.play_buffer.appendleft(
                self.main.current_output.vs_output.get_frame_async(
                    next_frame_for_buffer))
Esempio n. 21
0
    def run(self) -> None:
        from copy import deepcopy

        from vapoursynth import VideoFrame

        if self.main.BENCHMARK_CLEAR_CACHE:
            vs_clear_cache()
        if self.main.BENCHMARK_FRAME_DATA_SHARING_FIX:
            self.main.current_output.graphics_scene_item.setPixmap(
                self.main.current_output.graphics_scene_item.pixmap().copy())

        self.start_frame = self.start_frame_control.value()
        self.end_frame = self.end_frame_control.value()
        self.total_frames = self.total_frames_control.value()
        self.frames_left = deepcopy(self.total_frames)
        if self.prefetch_checkbox.isChecked():
            concurrent_requests_count = get_usable_cpus_count()
        else:
            concurrent_requests_count = 1

        self.unsequenced = self.unsequenced_checkbox.isChecked()
        if not self.unsequenced:
            self.buffer = deque([], concurrent_requests_count)
            self.sequenced_timer.start()

        self.running = True
        self.run_start_time = perf_counter()

        for offset in range(
                min(int(self.frames_left), concurrent_requests_count)):
            if self.unsequenced:
                self._request_next_frame_unsequenced()
            else:
                frame = self.start_frame + FrameInterval(offset)
                future = self.main.current_output.vs_output.get_frame_async(
                    int(frame))
                self.buffer.appendleft(future)

        self.update_info_timer.start()
Esempio n. 22
0
 def seek_to_next(self, checked: Optional[bool] = None) -> None:
     new_pos = self.main.current_frame + FrameInterval(1)
     if new_pos > self.main.current_output.end_frame:
         return
     self.stop()
     self.main.current_frame = new_pos
Esempio n. 23
0
class MainWindow(AbstractMainWindow):
    # those are defaults that can be overriden in runtime or used as fallbacks
    AUTOSAVE_ENABLED = True
    AUTOSAVE_INTERVAL = 30 * 1000  # s
    BASE_PPI = 96  # PPI
    DARK_THEME = True
    FPS_REFRESH_INTERVAL = 1000  # ms
    LOG_LEVEL = logging.DEBUG
    OPENGL_RENDERING = False
    OUTPUT_INDEX = 0
    PLAY_BUFFER_SIZE = FrameInterval(4)  # frames
    PNG_COMPRESSION_LEVEL = 80  # 0 - 100
    SAVE_TEMPLATE = '{script_name}_{frame}'
    SEEK_STEP = 1  # frames
    STATUSBAR_MESSAGE_TIMEOUT = 3 * 1000  # s
    # it's allowed to stretch target interval betweewn notches by 20% at most
    TIMELINE_LABEL_NOTCHES_MARGIN = 20  # %
    TIMELINE_MODE = 'frame'

    DEBUG_TOOLBAR = False
    DEBUG_TOOLBAR_BUTTONS_PRINT_STATE = False

    storable_attrs = ['toolbars']
    __slots__ = storable_attrs + [
        'app', 'opengl_widget', 'main_layout', 'main_toolbar_widget',
        'main_toolbar_layout', 'graphics_view', 'script_error_dialog'
        'outputs_combobox', 'frame_spinbox', 'copy_frame_button',
        'time_spinbox', 'copy_timestamp_button', 'test_button'
    ]

    yaml_tag = '!MainWindow'

    def __init__(self) -> None:
        from qdarkstyle import load_stylesheet_pyqt5

        super().__init__()

        # logging

        logging.basicConfig(format='{asctime}: {levelname}: {message}',
                            style='{',
                            level=self.LOG_LEVEL)
        logging.Formatter.default_msec_format = '%s.%03d'

        # ???

        self.app = Qt.QApplication.instance()
        if self.DARK_THEME:
            self.app.setStyleSheet(
                self.patch_dark_stylesheet(load_stylesheet_pyqt5()))
            self.ensurePolished()

        self.display_scale = self.app.primaryScreen().logicalDotsPerInch(
        ) / self.BASE_PPI
        self.setWindowTitle('VSPreview')
        self.move(400, 0)
        self.setup_ui()

        # global

        self.clipboard = self.app.clipboard()
        self.script_path = Path()
        self.save_on_exit = True

        # graphics view

        self.graphics_scene = Qt.QGraphicsScene(self)
        self.graphics_view.setScene(self.graphics_scene)
        if self.OPENGL_RENDERING:
            self.opengl_widget = Qt.QOpenGLWidget()
            self.graphics_view.setViewport(self.opengl_widget)

        self.graphics_view.wheelScrolled.connect(self.on_wheel_scrolled)

        # timeline

        self.timeline.clicked.connect(self.on_current_frame_changed)

        # init toolbars and outputs

        self.toolbars = Toolbars(self)
        self.main_layout.addWidget(self.toolbars.main)
        for toolbar in self.toolbars:
            self.main_layout.addWidget(toolbar)
            self.toolbars.main.layout().addWidget(toolbar.toggle_button)

    def setup_ui(self) -> None:
        from vspreview.widgets import GraphicsView

        # mainWindow.resize(1300, 808)

        self.central_widget = Qt.QWidget(self)
        self.main_layout = Qt.QVBoxLayout(self.central_widget)
        self.setCentralWidget(self.central_widget)

        self.graphics_view = GraphicsView(self.central_widget)
        # self.graphics_view.setOptimizationFlag(Qt.QGraphicsView.OptimizationFlag.DontSavePainterState)
        # self.graphics_view.setOptimizationFlag(Qt.QGraphicsView.OptimizationFlag.DontAdjustForAntialiasing)
        self.graphics_view.setBackgroundBrush(self.palette().brush(
            Qt.QPalette.Window))
        self.graphics_view.setSizePolicy(Qt.QSizePolicy.Fixed,
                                         Qt.QSizePolicy.Fixed)
        self.graphics_view.setDragMode(Qt.QGraphicsView.ScrollHandDrag)
        self.main_layout.addWidget(self.graphics_view)

        self.timeline = Timeline(self.central_widget)
        self.main_layout.addWidget(self.timeline)

        # status bar

        self.statusbar = Qt.QStatusBar(self.central_widget)

        self.statusbar.total_frames_label = Qt.QLabel(self.central_widget)
        self.statusbar.addWidget(self.statusbar.total_frames_label)

        self.statusbar.duration_label = Qt.QLabel(self.central_widget)
        self.statusbar.addWidget(self.statusbar.duration_label)

        self.statusbar.resolution_label = Qt.QLabel(self.central_widget)
        self.statusbar.addWidget(self.statusbar.resolution_label)

        self.statusbar.pixel_format_label = Qt.QLabel(self.central_widget)
        self.statusbar.addWidget(self.statusbar.pixel_format_label)

        self.statusbar.fps_label = Qt.QLabel(self.central_widget)
        self.statusbar.addWidget(self.statusbar.fps_label)

        self.statusbar.label = Qt.QLabel(self.central_widget)
        self.statusbar.addPermanentWidget(self.statusbar.label)

        self.setStatusBar(self.statusbar)

        # dialogs

        self.script_error_dialog = ScriptErrorDialog(self)

    def patch_dark_stylesheet(self, stylesheet: str) -> str:
        return stylesheet + 'QGraphicsView { border: 0px; padding: 0px; }'

    def load_script(self, script_path: Path) -> None:
        from traceback import print_exc

        self.toolbars.playback.stop()

        self.statusbar.label.setText('Evaluating')
        self.script_path = script_path
        sys.path.append(str(self.script_path.parent))
        try:
            exec(self.script_path.read_text(), {})  # pylint: disable=exec-used
        except Exception:  # pylint: disable=broad-except
            logging.error(
                'Script contains error(s). Check following lines for details.')
            self.handle_script_error(
                'Script contains error(s). See console output for details.')
            print_exc()
            return
        finally:
            sys.path.pop()

        if len(vs.get_outputs()) == 0:
            logging.error('Script has no outputs set.')
            self.handle_script_error('Script has no outputs set.')
            return

        self.toolbars.main.rescan_outputs()
        # self.init_outputs()
        self.switch_output(self.OUTPUT_INDEX)

        self.load_storage()

    def load_storage(self) -> None:
        import yaml

        storage_path = self.script_path.with_suffix('.yml')
        if storage_path.exists():
            try:
                yaml.load(storage_path.open(), Loader=yaml.Loader)
            except yaml.YAMLError as exc:
                if isinstance(exc, yaml.MarkedYAMLError):
                    logging.warning(
                        'Storage parsing failed on line {} column {}. Using defaults.'
                        .format(exc.problem_mark.line + 1,
                                exc.problem_mark.column + 1))  # pylint: disable=no-member
                else:
                    logging.warning('Storage parsing failed. Using defaults.')
                # logging.getLogger().setLevel(logging.ERROR)
        else:
            logging.info('No storage found. Using defaults.')
            # logging.getLogger().setLevel(logging.ERROR)

        # logging.getLogger().setLevel(self.LOG_LEVEL)

        self.statusbar.label.setText('Ready')

    def init_outputs(self) -> None:
        self.graphics_scene.clear()
        for output in self.outputs:
            frame_pixmap = self.render_frame(output.last_showed_frame, output)
            frame_item = self.graphics_scene.addPixmap(frame_pixmap)
            frame_item.hide()
            output.graphics_scene_item = frame_item

    def reload_script(self) -> None:
        if self.toolbars.misc.autosave_enabled:
            self.toolbars.misc.save()
        vs.clear_outputs()
        self.graphics_scene.clear()
        self.load_script(self.script_path)

        self.statusbar.showMessage('Reloaded successfully',
                                   self.STATUSBAR_MESSAGE_TIMEOUT)

    def render_frame(self,
                     frame: Frame,
                     output: Optional[Output] = None) -> Qt.QPixmap:
        if output is None:
            output = self.current_output

        return self.render_raw_videoframe(
            output.vs_output.get_frame(int(frame)))

    def render_raw_videoframe(self, vs_frame: vs.VideoFrame) -> Qt.QPixmap:
        import ctypes

        frame_data = vs_frame.get_read_ptr(0)
        frame_stride = vs_frame.get_stride(0)
        # frame_itemsize = vs_frame.get_read_array(0).itemsize
        frame_itemsize = vs_frame.format.bytes_per_sample

        # powerful spell. do not touch
        frame_data = ctypes.cast(
            frame_data,
            ctypes.POINTER(ctypes.c_char *
                           (frame_itemsize * vs_frame.width *
                            vs_frame.height)))[0]  # type: ignore
        frame_image = Qt.QImage(frame_data, vs_frame.width, vs_frame.height,
                                frame_stride, Qt.QImage.Format_RGB32)
        frame_pixmap = Qt.QPixmap.fromImage(frame_image)

        return frame_pixmap

    def on_current_frame_changed(self,
                                 frame: Optional[Frame] = None,
                                 t: Optional[timedelta] = None,
                                 render_frame: bool = True) -> None:
        if t is None and frame is not None:
            t = self.to_timedelta(frame)
        elif t is not None and frame is None:
            frame = self.to_frame(t)
        elif t is not None and frame is not None:
            pass
        else:
            logging.debug(
                'on_current_frame_changed(): both frame and t is None')
            return
        if frame >= self.current_output.total_frames:
            # logging.debug('on_current_frame_changed(): New frame position is out of range')
            return

        self.current_output.last_showed_frame = frame

        self.timeline.set_position(frame)
        self.toolbars.main.on_current_frame_changed(frame, t)
        for toolbar in self.toolbars:
            toolbar.on_current_frame_changed(frame, t)

        if render_frame:
            self.current_output.graphics_scene_item.setPixmap(
                self.render_frame(frame, self.current_output))

    def switch_output(self, index: int) -> None:
        if len(self.outputs) == 0:
            # TODO: consider returning False
            return

        # print(index)
        # print_stack()

        prev_index = self.toolbars.main.outputs_combobox.currentIndex()
        if index < 0 or index >= len(self.outputs):
            logging.info(
                'Output switching: output index is out of range. Switching to first output'
            )
            index = 0

        self.toolbars.playback.stop()

        # current_output relies on outputs_combobox
        self.toolbars.main.on_current_output_changed(index, prev_index)
        self.timeline.set_duration(self.current_output.total_frames,
                                   self.current_output.duration)
        self.current_frame = self.current_output.last_showed_frame

        for output in self.outputs:
            output.graphics_scene_item.hide()
        self.current_output.graphics_scene_item.show()
        self.graphics_scene.setSceneRect(
            Qt.QRectF(self.current_output.graphics_scene_item.pixmap().rect()))
        self.timeline.update_notches()
        for toolbar in self.toolbars:
            toolbar.on_current_output_changed(index, prev_index)
        self.update_statusbar_output_info()

    @property
    def current_output(self) -> Output:  # type: ignore
        output = cast(Output,
                      self.toolbars.main.outputs_combobox.currentData())
        # check currentData() return on empty combobox
        # if data != '':
        #    return cast(Output, data)
        # return None
        return output

    @property  # type: ignore
    def current_frame(self) -> Frame:  # type: ignore
        # if self.current_output is None:
        #     return None
        return self.current_output.last_showed_frame

    @current_frame.setter
    def current_frame(self, value: Frame) -> None:
        self.on_current_frame_changed(value)

    @property
    def outputs(self) -> Outputs:  # type: ignore
        return cast(Outputs, self.toolbars.main.outputs)

    def handle_script_error(self, message: str) -> None:
        # logging.error(message)
        self.script_error_dialog.label.setText(message)
        self.script_error_dialog.open()

    def on_wheel_scrolled(self, steps: int) -> None:
        new_index = self.toolbars.main.zoom_combobox.currentIndex() + steps
        if new_index < 0:
            new_index = 0
        elif new_index >= len(self.toolbars.main.zoom_levels):
            new_index = len(self.toolbars.main.zoom_levels) - 1
        self.toolbars.main.zoom_combobox.setCurrentIndex(new_index)

    def update_statusbar_output_info(self,
                                     output: Optional[Output] = None) -> None:
        from vspreview.utils import strfdelta

        if output is None:
            output = self.current_output

        self.statusbar.total_frames_label.setText('{} frames '.format(
            output.total_frames))
        self.statusbar.duration_label.setText('{} '.format(
            strfdelta(output.duration, '%H:%M:%S.%Z')))
        self.statusbar.resolution_label.setText('{}x{} '.format(
            output.width, output.height))
        self.statusbar.pixel_format_label.setText('{} '.format(
            output.format.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))

    # misc methods

    def showEvent(self, event: Qt.QShowEvent) -> None:
        super().showEvent(event)
        self.graphics_view.setSizePolicy(
            Qt.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding))

    def closeEvent(self, event: Qt.QCloseEvent) -> None:
        if (self.toolbars.misc.autosave_enabled and self.save_on_exit):
            self.toolbars.misc.save()

    def to_frame(self, t: timedelta) -> Frame:
        return Frame(
            round(t.total_seconds() *
                  (self.current_output.fps_num / self.current_output.fps_den)))

    def to_timedelta(self, frame: Frame) -> timedelta:
        return timedelta(seconds=(
            int(frame) /
            (self.current_output.fps_num / self.current_output.fps_den)))

    def __getstate__(self) -> Mapping[str, Any]:
        state = {
            attr_name: getattr(self, attr_name)
            for attr_name in self.storable_attrs
        }
        state.update({'timeline_mode': self.timeline.mode})
        return state

    def __setstate__(self, state: Mapping[str, Any]) -> None:
        # toolbars is singleton, so it initialize itself right in its __setstate__()

        try:
            timeline_mode = state['timeline_mode']
            if not Timeline.Mode.is_valid(timeline_mode):
                raise TypeError
        except (KeyError, TypeError):
            logging.warning(
                'Storage loading: failed to parse timeline mode. Using default.'
            )
            timeline_mode = self.TIMELINE_MODE
        self.timeline.mode = timeline_mode
Esempio n. 24
0
    def setup_ui(self) -> None:
        layout = Qt.QHBoxLayout(self)
        layout.setObjectName('PlaybackToolbar.setup_ui.layout')
        layout.setContentsMargins(0, 0, 0, 0)

        self.seek_to_start_button = Qt.QToolButton(self)
        self.seek_to_start_button.setText('⏮')
        self.seek_to_start_button.setToolTip('Seek to First Frame')
        layout.addWidget(self.seek_to_start_button)

        self.seek_n_frames_b_button = Qt.QToolButton(self)
        self.seek_n_frames_b_button.setText('⏪')
        self.seek_n_frames_b_button.setToolTip('Seek N Frames Backwards')
        layout.addWidget(self.seek_n_frames_b_button)

        self.seek_to_prev_button = Qt.QToolButton(self)
        self.seek_to_prev_button.setText('◂')
        self.seek_to_prev_button.setToolTip('Seek 1 Frame Backwards')
        layout.addWidget(self.seek_to_prev_button)

        self.play_pause_button = Qt.QToolButton(self)
        self.play_pause_button.setText('⏯')
        self.play_pause_button.setToolTip('Play/Pause')
        self.play_pause_button.setCheckable(True)
        layout.addWidget(self.play_pause_button)

        self.seek_to_next_button = Qt.QToolButton(self)
        self.seek_to_next_button.setText('▸')
        self.seek_to_next_button.setToolTip('Seek 1 Frame Forward')
        layout.addWidget(self.seek_to_next_button)

        self.seek_n_frames_f_button = Qt.QToolButton(self)
        self.seek_n_frames_f_button.setText('⏩')
        self.seek_n_frames_f_button.setToolTip('Seek N Frames Forward')
        layout.addWidget(self.seek_n_frames_f_button)

        self.seek_to_end_button = Qt.QToolButton(self)
        self.seek_to_end_button.setText('⏭')
        self.seek_to_end_button.setToolTip('Seek to Last Frame')
        layout.addWidget(self.seek_to_end_button)

        self.seek_frame_control = FrameEdit[FrameInterval](self)
        self.seek_frame_control.setMinimum(FrameInterval(1))
        self.seek_frame_control.setToolTip('Seek N Frames Step')
        self.seek_frame_control.setValue(FrameInterval(self.main.SEEK_STEP))
        layout.addWidget(self.seek_frame_control)

        self.seek_time_control = TimeEdit[TimeInterval](self)
        layout.addWidget(self.seek_time_control)

        self.fps_spinbox = Qt.QDoubleSpinBox(self)
        self.fps_spinbox.setRange(0.001, 9999.0)
        self.fps_spinbox.setDecimals(3)
        self.fps_spinbox.setSuffix(' fps')
        layout.addWidget(self.fps_spinbox)

        self.fps_reset_button = Qt.QPushButton(self)
        self.fps_reset_button.setText('Reset FPS')
        layout.addWidget(self.fps_reset_button)

        self.fps_unlimited_checkbox = Qt.QCheckBox(self)
        self.fps_unlimited_checkbox.setText('Unlimited FPS')
        layout.addWidget(self.fps_unlimited_checkbox)

        layout.addStretch()
Esempio n. 25
0
class MainWindow(AbstractMainWindow):
    # those are defaults that can be overriden at runtime or used as fallbacks
    ALWAYS_SHOW_SCENE_MARKS = False
    AUTOSAVE_INTERVAL = 60 * 1000  # s
    BASE_PPI = 96  # PPI
    BENCHMARK_CLEAR_CACHE = False
    BENCHMARK_REFRESH_INTERVAL = 150  # ms
    CHECKERBOARD_ENABLED = True
    CHECKERBOARD_TILE_COLOR_1 = Qt.Qt.white
    CHECKERBOARD_TILE_COLOR_2 = Qt.Qt.lightGray
    CHECKERBOARD_TILE_SIZE = 8  # px
    DARK_THEME = True
    FPS_AVERAGING_WINDOW_SIZE = FrameInterval(100)
    FPS_REFRESH_INTERVAL = 150  # ms
    LOG_LEVEL = logging.DEBUG
    OPENGL_RENDERING = False
    ORDERED_OUTPUTS = False
    OUTPUT_INDEX = 0
    PLAY_BUFFER_SIZE = FrameInterval(get_usable_cpus_count())
    PNG_COMPRESSION_LEVEL = 0  # 0 - 100
    SAVE_TEMPLATE = '{script_name}_{frame}'
    SEEK_STEP = 1  # frames
    STATUSBAR_MESSAGE_TIMEOUT = 3 * 1000  # s
    STORAGE_BACKUPS_COUNT = 2
    SYNC_OUTPUTS = False
    # it's allowed to stretch target interval betweewn notches by N% at most
    TIMELINE_LABEL_NOTCHES_MARGIN = 20  # %
    TIMELINE_MODE = 'frame'
    TOGGLE_TOOLBAR = False
    VSP_DIR_NAME = '.vspreview'
    # used for formats with subsampling
    VS_OUTPUT_RESIZER = Output.Resizer.Bicubic
    VS_OUTPUT_MATRIX = Output.Matrix.BT709
    VS_OUTPUT_TRANSFER = Output.Transfer.BT709
    VS_OUTPUT_PRIMARIES = Output.Primaries.BT709
    VS_OUTPUT_RANGE = Output.Range.LIMITED
    VS_OUTPUT_CHROMALOC = Output.ChromaLoc.LEFT
    VS_OUTPUT_PREFER_PROPS = True
    VS_OUTPUT_RESIZER_KWARGS = {}  # type: Mapping[str, str]

    BENCHMARK_FRAME_DATA_SHARING_FIX = True
    DEBUG_PLAY_FPS = False
    DEBUG_TOOLBAR = False
    DEBUG_TOOLBAR_BUTTONS_PRINT_STATE = False

    yaml_tag = '!MainWindow'

    storable_attrs = [
        'toolbars',
    ]
    __slots__ = storable_attrs + [
        'app', 'display_scale', 'clipboard', 'script_path', 'save_on_exit',
        'timeline', 'main_layout', 'graphics_scene', 'graphics_view',
        'script_error_dialog', 'central_widget', 'statusbar', 'opengl_widget',
        'external_args', 'script_exec_failed'
    ]

    def __init__(self) -> None:
        from qdarkstyle import load_stylesheet_pyqt5

        super().__init__()

        # logging

        # ???

        self.app = Qt.QApplication.instance()
        if self.DARK_THEME:
            self.app.setStyleSheet(
                self.patch_dark_stylesheet(load_stylesheet_pyqt5()))
            self.ensurePolished()

        self.display_scale = self.app.primaryScreen().logicalDotsPerInch(
        ) / self.BASE_PPI
        self.setWindowTitle('VSPreview')
        self.move(400, 0)
        self.setup_ui()

        # global

        self.clipboard = self.app.clipboard()
        self.external_args: List[str] = []
        self.script_path = Path()
        self.save_on_exit = True
        self.script_exec_failed = False

        # graphics view

        self.graphics_scene = Qt.QGraphicsScene(self)
        self.graphics_view.setScene(self.graphics_scene)
        self.opengl_widget = None
        if self.OPENGL_RENDERING:
            self.opengl_widget = Qt.QOpenGLWidget()
            self.graphics_view.setViewport(self.opengl_widget)

        self.graphics_view.wheelScrolled.connect(self.on_wheel_scrolled)

        # timeline

        self.timeline.clicked.connect(self.switch_frame)

        # init toolbars and outputs

        self.toolbars = Toolbars(self)
        self.main_layout.addWidget(self.toolbars.main)
        for toolbar in self.toolbars:
            self.main_layout.addWidget(toolbar)
            self.toolbars.main.layout().addWidget(toolbar.toggle_button)

        set_qobject_names(self)
        self.setObjectName('MainWindow')

    def setup_ui(self) -> None:
        from vspreview.widgets import GraphicsView

        # mainWindow.resize(1300, 808)

        self.central_widget = Qt.QWidget(self)
        self.main_layout = Qt.QVBoxLayout(self.central_widget)
        self.setCentralWidget(self.central_widget)

        self.graphics_view = GraphicsView(self.central_widget)
        self.graphics_view.setBackgroundBrush(self.palette().brush(
            Qt.QPalette.Window))
        self.graphics_view.setSizePolicy(Qt.QSizePolicy.Fixed,
                                         Qt.QSizePolicy.Fixed)
        self.graphics_view.setDragMode(Qt.QGraphicsView.ScrollHandDrag)
        self.graphics_view.setTransformationAnchor(
            GraphicsView.AnchorUnderMouse)
        self.main_layout.addWidget(self.graphics_view)

        self.timeline = Timeline(self.central_widget)
        self.main_layout.addWidget(self.timeline)

        # status bar

        self.statusbar = StatusBar(self.central_widget)

        self.statusbar.total_frames_label = Qt.QLabel(self.central_widget)
        self.statusbar.total_frames_label.setObjectName(
            'MainWindow.statusbar.total_frames_label')
        self.statusbar.addWidget(self.statusbar.total_frames_label)

        self.statusbar.duration_label = Qt.QLabel(self.central_widget)
        self.statusbar.duration_label.setObjectName(
            'MainWindow.statusbar.duration_label')
        self.statusbar.addWidget(self.statusbar.duration_label)

        self.statusbar.resolution_label = Qt.QLabel(self.central_widget)
        self.statusbar.resolution_label.setObjectName(
            'MainWindow.statusbar.resolution_label')
        self.statusbar.addWidget(self.statusbar.resolution_label)

        self.statusbar.pixel_format_label = Qt.QLabel(self.central_widget)
        self.statusbar.pixel_format_label.setObjectName(
            'MainWindow.statusbar.pixel_format_label')
        self.statusbar.addWidget(self.statusbar.pixel_format_label)

        self.statusbar.fps_label = Qt.QLabel(self.central_widget)
        self.statusbar.fps_label.setObjectName(
            'MainWindow.statusbar.fps_label')
        self.statusbar.addWidget(self.statusbar.fps_label)

        self.statusbar.label = Qt.QLabel(self.central_widget)
        self.statusbar.label.setObjectName('MainWindow.statusbar.label')
        self.statusbar.addPermanentWidget(self.statusbar.label)

        self.setStatusBar(self.statusbar)

        # dialogs

        self.script_error_dialog = ScriptErrorDialog(self)

    def patch_dark_stylesheet(self, stylesheet: str) -> str:
        return stylesheet \
            + ' QGraphicsView { border: 0px; padding: 0px; }' \
            + ' QToolButton { padding: 0px; }'

    def load_script(self,
                    script_path: Path,
                    external_args: str = '',
                    reloading=False) -> None:
        import shlex
        from traceback import FrameSummary, TracebackException

        self.toolbars.playback.stop()

        self.statusbar.label.setText('Evaluating')
        self.script_path = script_path
        sys.path.append(str(self.script_path.parent))

        # Rewrite args so external args will be forwarded correctly
        if external_args:
            self.external_args = shlex.split(external_args)
        try:
            argv_orig = sys.argv
            sys.argv = [script_path.name] + self.external_args
        except AttributeError:
            pass

        try:
            # pylint: disable=exec-used
            exec(self.script_path.read_text(encoding='utf-8'),
                 {'__file__': sys.argv[0]})
        except Exception as e:  # pylint: disable=broad-except
            self.script_exec_failed = True
            logging.error(e)

            te = TracebackException.from_exception(e)
            # remove the first stack frame, which contains our exec() invocation
            del te.stack[0]

            # replace <string> with script path only for the first stack frames
            # in order to keep intact exec() invocations down the stack
            # that we're not concerned with
            for i, frame in enumerate(te.stack):
                if frame.filename == '<string>':
                    te.stack[i] = FrameSummary(str(self.script_path),
                                               frame.lineno, frame.name)
                else:
                    break
            print(''.join(te.format()))

            self.handle_script_error(
                f'''An error occured while evaluating script:
                \n{str(e)}
                \nSee console output for details.''')
            return
        finally:
            sys.argv = argv_orig
            sys.path.pop()

        self.script_exec_failed = False

        if len(vs.get_outputs()) == 0:
            logging.error('Script has no outputs set.')
            self.handle_script_error('Script has no outputs set.')
            return

        if not reloading:
            self.toolbars.main.rescan_outputs()
            for toolbar in self.toolbars:
                toolbar.on_script_loaded()
            self.switch_output(self.OUTPUT_INDEX)

            self.load_storage()
        else:
            self.load_storage()
            for toolbar in self.toolbars:
                toolbar.on_script_loaded()

    def load_storage(self) -> None:
        import yaml

        vsp_dir = self.script_path.parent / self.VSP_DIR_NAME
        storage_path = vsp_dir / (self.script_path.stem + '.yml')

        if not storage_path.exists():
            storage_path = self.script_path.with_suffix('.yml')

        if storage_path.exists():
            try:
                yaml.load(storage_path.open(), Loader=yaml.Loader)
            except yaml.YAMLError as exc:
                if isinstance(exc, yaml.MarkedYAMLError):
                    logging.warning(
                        'Storage parsing failed at line {}:{} ({} {}).'
                        'Using defaults.'.format(exc.problem_mark.line + 1,
                                                 exc.problem_mark.column + 1,
                                                 exc.problem, exc.context))  # pylint: disable=no-member
                else:
                    logging.warning('Storage parsing failed. Using defaults.')
        else:
            logging.info('No storage found. Using defaults.')

        self.statusbar.label.setText('Ready')

    def init_outputs(self) -> None:
        from vspreview.widgets import GraphicsImageItem

        self.graphics_scene.clear()
        for output in self.outputs:
            frame_image = output.render_frame(output.last_showed_frame)

            raw_frame_item = self.graphics_scene.addPixmap(
                Qt.QPixmap.fromImage(frame_image))
            raw_frame_item.hide()

            frame_item = GraphicsImageItem(raw_frame_item, frame_image)
            output.graphics_scene_item = frame_item

    def reload_script(self) -> None:
        import gc

        if not self.script_exec_failed:
            self.toolbars.misc.save_sync()
        for toolbar in self.toolbars:
            toolbar.on_script_unloaded()

        vs.clear_outputs()
        self.graphics_scene.clear()
        self.outputs.clear()
        # make sure old filter graph is freed
        gc.collect()

        self.load_script(self.script_path, reloading=True)

        self.show_message('Reloaded successfully')

    def render_frame(self,
                     frame: Frame,
                     output: Optional[Output] = None) -> Qt.QImage:
        return self.current_output.render_frame(frame)

    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_output(self, value: Union[int, Output]) -> None:
        if len(self.outputs) == 0:
            return
        if isinstance(value, Output):
            index = self.outputs.index_of(value)
        else:
            index = value

        if index < 0 or index >= len(self.outputs):
            return

        prev_index = self.toolbars.main.outputs_combobox.currentIndex()

        self.toolbars.playback.stop()

        # current_output relies on outputs_combobox
        self.toolbars.main.on_current_output_changed(index, prev_index)
        self.timeline.set_end_frame(self.current_output.end_frame)
        if self.current_output.frame_to_show is not None:
            self.current_frame = self.current_output.frame_to_show
        else:
            self.current_frame = self.current_output.last_showed_frame

        for output in self.outputs:
            output.graphics_scene_item.hide()
        self.current_output.graphics_scene_item.show()
        self.graphics_scene.setSceneRect(
            Qt.QRectF(self.current_output.graphics_scene_item.pixmap().rect()))
        self.timeline.update_notches()
        for toolbar in self.toolbars:
            toolbar.on_current_output_changed(index, prev_index)
        self.update_statusbar_output_info()

    @property  # type: ignore
    def current_output(self) -> Output:  # type: ignore
        output = cast(Output,
                      self.toolbars.main.outputs_combobox.currentData())
        return output

    @current_output.setter
    def current_output(self, value: Output) -> None:
        self.switch_output(self.outputs.index_of(value))

    @property  # type: ignore
    def current_frame(self) -> Frame:  # type: ignore
        return self.current_output.last_showed_frame

    @current_frame.setter
    def current_frame(self, value: Frame) -> None:
        self.switch_frame(value)

    @property  # type: ignore
    def current_time(self) -> Time:  # type: ignore
        return Time(self.current_output.last_showed_frame)

    @current_time.setter
    def current_time(self, value: Time) -> None:
        self.switch_frame(value)

    @property
    def outputs(self) -> Outputs:  # type: ignore
        return cast(Outputs, self.toolbars.main.outputs)

    def handle_script_error(self, message: str) -> None:
        # logging.error(message)
        self.script_error_dialog.label.setText(message)
        self.script_error_dialog.open()

    def on_wheel_scrolled(self, steps: int) -> None:
        new_index = self.toolbars.main.zoom_combobox.currentIndex() + steps
        if new_index < 0:
            new_index = 0
        elif new_index >= len(self.toolbars.main.zoom_levels):
            new_index = len(self.toolbars.main.zoom_levels) - 1
        self.toolbars.main.zoom_combobox.setCurrentIndex(new_index)

    def show_message(self,
                     message: str,
                     timeout: Optional[int] = None) -> None:
        if timeout is None:
            timeout = self.STATUSBAR_MESSAGE_TIMEOUT
        self.statusbar.showMessage(message, timeout)

    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 event(self, event: Qt.QEvent) -> bool:
        if event.type() == Qt.QEvent.LayoutRequest:
            self.timeline.full_repaint()

        return super().event(event)

    # misc methods

    def showEvent(self, event: Qt.QShowEvent) -> None:
        super().showEvent(event)
        self.graphics_view.setSizePolicy(
            Qt.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding))

    def closeEvent(self, event: Qt.QCloseEvent) -> None:
        if self.save_on_exit:
            self.toolbars.misc.save()

    def __getstate__(self) -> Mapping[str, Any]:
        state = {
            attr_name: getattr(self, attr_name)
            for attr_name in self.storable_attrs
        }
        state.update({
            'timeline_mode': self.timeline.mode,
            'window_geometry': bytes(self.saveGeometry()),
            'window_state': bytes(self.saveState()),
        })
        return state

    def __setstate__(self, state: Mapping[str, Any]) -> None:
        # toolbars is singleton,
        # so it initialize itself right in its __setstate__()

        try:
            timeline_mode = state['timeline_mode']
            if not Timeline.Mode.is_valid(timeline_mode):
                raise TypeError
        except (KeyError, TypeError):
            logging.warning('Storage loading: failed to parse timeline mode.'
                            ' Using default.')
            timeline_mode = self.TIMELINE_MODE
        self.timeline.mode = timeline_mode

        try:
            window_geometry = state['window_geometry']
            if not isinstance(window_geometry, bytes):
                raise TypeError
            self.restoreGeometry(window_geometry)
        except (KeyError, TypeError):
            logging.warning('Storage loading: failed to parse window geometry.'
                            ' Using default.')

        try:
            window_state = state['window_state']
            if not isinstance(window_state, bytes):
                raise TypeError
            self.restoreState(window_state)
        except (KeyError, TypeError):
            logging.warning('Storage loading: failed to parse window state.'
                            ' Using default.')
Esempio n. 26
0
    def setup_ui(self) -> None:
        layout = Qt.QHBoxLayout(self)
        layout.setObjectName('BenchmarkToolbar.setup_ui.layout')
        layout.setContentsMargins(0, 0, 0, 0)

        start_label = Qt.QLabel(self)
        start_label.setObjectName('BenchmarkToolbar.setup_ui.start_label')
        start_label.setText('Start:')
        layout.addWidget(start_label)

        self.start_frame_control = FrameEdit[Frame](self)
        layout.addWidget(self.start_frame_control)

        self.start_time_control = TimeEdit[Time](self)
        layout.addWidget(self.start_time_control)

        end_label = Qt.QLabel(self)
        end_label.setObjectName('BenchmarkToolbar.setup_ui.end_label')
        end_label.setText('End:')
        layout.addWidget(end_label)

        self.end_frame_control = FrameEdit[Frame](self)
        layout.addWidget(self.end_frame_control)

        self.end_time_control = TimeEdit[Time](self)
        layout.addWidget(self.end_time_control)

        total_label = Qt.QLabel(self)
        total_label.setObjectName('BenchmarkToolbar.setup_ui.total_label')
        total_label.setText('Total:')
        layout.addWidget(total_label)

        self.total_frames_control = FrameEdit[FrameInterval](self)
        self.total_frames_control.setMinimum(FrameInterval(1))
        layout.addWidget(self.total_frames_control)

        self.total_time_control = TimeEdit[TimeInterval](self)
        layout.addWidget(self.total_time_control)

        self.prefetch_checkbox = Qt.QCheckBox(self)
        self.prefetch_checkbox.setText('Prefetch')
        self.prefetch_checkbox.setChecked(True)
        self.prefetch_checkbox.setToolTip(
            'Request multiple frames in advance.')
        layout.addWidget(self.prefetch_checkbox)

        self.unsequenced_checkbox = Qt.QCheckBox(self)
        self.unsequenced_checkbox.setText('Unsequenced')
        self.unsequenced_checkbox.setChecked(True)
        self.unsequenced_checkbox.setToolTip(
            "If enabled, next frame will be requested each time "
            "frameserver returns completed frame. "
            "If disabled, first frame that's currently processing "
            "will be waited before requesting next. Like for playback. ")
        layout.addWidget(self.unsequenced_checkbox)

        self.run_abort_button = Qt.QPushButton(self)
        self.run_abort_button.setText('Run')
        self.run_abort_button.setCheckable(True)
        layout.addWidget(self.run_abort_button)

        self.info_label = Qt.QLabel(self)
        layout.addWidget(self.info_label)

        layout.addStretch()
Esempio n. 27
0
 def calculate_notch_interval_f(self, target_interval_x: int) -> FrameInterval:
     intervals = (
         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),
     )
     margin  = 1 + self.main.TIMELINE_LABEL_NOTCHES_MARGIN / 100
     target_interval_f = self.x_to_f(target_interval_x)
     for interval in intervals:
         if target_interval_f < interval * margin:
             return interval
     return intervals[-1]
Esempio n. 28
0
 def on_seek_time_changed(self, time: TimeInterval) -> None:
     qt_silent_call(self.seek_frame_control.setValue, FrameInterval(time))
Esempio n. 29
0
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)
Esempio n. 30
0
class MainWindow(AbstractMainWindow):
    # those are defaults that can be overriden at runtime or used as fallbacks
    AUTOSAVE_ENABLED = True
    AUTOSAVE_INTERVAL = 30 * 1000  # s
    BASE_PPI = 96  # PPI
    BENCHMARK_CLEAR_CACHE = False
    BENCHMARK_REFRESH_INTERVAL = 150  # ms
    DARK_THEME = True
    FPS_AVERAGING_WINDOW_SIZE = FrameInterval(100)
    FPS_REFRESH_INTERVAL = 150  # ms
    LOG_LEVEL = logging.DEBUG
    OPENGL_RENDERING = False
    OUTPUT_INDEX = 0
    PLAY_BUFFER_SIZE = FrameInterval(get_usable_cpus_count())
    PNG_COMPRESSION_LEVEL = 0  # 0 - 100
    SAVE_TEMPLATE = '{script_name}_{frame}'
    SEEK_STEP = 1  # frames
    STATUSBAR_MESSAGE_TIMEOUT = 3 * 1000  # s
    # it's allowed to stretch target interval betweewn notches by N% at most
    TIMELINE_LABEL_NOTCHES_MARGIN = 20  # %
    TIMELINE_MODE = 'frame'
    # would be used for formats with subsampling
    VS_OUTPUT_RESIZER = Output.Resizer.Bicubic
    VS_OUTPUT_MATRIX = Output.Matrix.BT709
    VS_OUTPUT_TRANSFER = Output.Transfer.BT709
    VS_OUTPUT_PRIMARIES = Output.Primaries.BT709
    VS_OUTPUT_RANGE = Output.Range.LIMITED
    VS_OUTPUT_CHROMALOC = Output.ChromaLoc.LEFT
    VS_OUTPUT_PREFER_PROPS = True
    VS_OUTPUT_RESIZER_KWARGS = {}  # type: Mapping[str, str]

    BENCHMARK_FRAME_DATA_SHARING_FIX = True
    DEBUG_PLAY_FPS = False
    DEBUG_TOOLBAR = False
    DEBUG_TOOLBAR_BUTTONS_PRINT_STATE = False

    yaml_tag = '!MainWindow'

    storable_attrs = [
        'toolbars',
    ]
    __slots__ = storable_attrs + [
        'app',
        'display_scale',
        'clipboard',
        'script_path',
        'save_on_exit',
        'timeline',
        'main_layout',
        'graphics_scene',
        'graphics_view',
        'script_error_dialog',
        'central_widget',
        'statusbar',
        'opengl_widget',
    ]

    def __init__(self) -> None:
        from qdarkstyle import load_stylesheet_pyqt5

        super().__init__()

        # logging

        logging.basicConfig(format='{asctime}: {levelname}: {message}',
                            style='{',
                            level=self.LOG_LEVEL)
        logging.Formatter.default_msec_format = '%s.%03d'

        # ???

        self.app = Qt.QApplication.instance()
        if self.DARK_THEME:
            self.app.setStyleSheet(
                self.patch_dark_stylesheet(load_stylesheet_pyqt5()))
            self.ensurePolished()

        self.display_scale = self.app.primaryScreen().logicalDotsPerInch(
        ) / self.BASE_PPI
        self.setWindowTitle('VSPreview')
        self.move(400, 0)
        self.setup_ui()

        # global

        self.clipboard = self.app.clipboard()
        self.script_path = Path()
        self.save_on_exit = True

        # graphics view

        self.graphics_scene = Qt.QGraphicsScene(self)
        self.graphics_view.setScene(self.graphics_scene)
        self.opengl_widget = None
        if self.OPENGL_RENDERING:
            self.opengl_widget = Qt.QOpenGLWidget()
            self.graphics_view.setViewport(self.opengl_widget)

        self.graphics_view.wheelScrolled.connect(self.on_wheel_scrolled)

        # timeline

        self.timeline.clicked.connect(self.switch_frame)

        # init toolbars and outputs

        self.toolbars = Toolbars(self)
        self.main_layout.addWidget(self.toolbars.main)
        for toolbar in self.toolbars:
            self.main_layout.addWidget(toolbar)
            self.toolbars.main.layout().addWidget(toolbar.toggle_button)

        set_qobject_names(self)
        self.setObjectName('MainWindow')

    def setup_ui(self) -> None:
        from vspreview.widgets import GraphicsView

        # mainWindow.resize(1300, 808)

        self.central_widget = Qt.QWidget(self)
        self.main_layout = Qt.QVBoxLayout(self.central_widget)
        self.setCentralWidget(self.central_widget)

        self.graphics_view = GraphicsView(self.central_widget)
        self.graphics_view.setBackgroundBrush(self.palette().brush(
            Qt.QPalette.Window))
        self.graphics_view.setSizePolicy(Qt.QSizePolicy.Fixed,
                                         Qt.QSizePolicy.Fixed)
        self.graphics_view.setDragMode(Qt.QGraphicsView.ScrollHandDrag)
        self.graphics_view.setTransformationAnchor(
            GraphicsView.AnchorUnderMouse)
        self.main_layout.addWidget(self.graphics_view)

        self.timeline = Timeline(self.central_widget)
        self.main_layout.addWidget(self.timeline)

        # status bar

        self.statusbar = StatusBar(self.central_widget)

        self.statusbar.total_frames_label = Qt.QLabel(self.central_widget)
        self.statusbar.total_frames_label.setObjectName(
            'MainWindow.statusbar.total_frames_label')
        self.statusbar.addWidget(self.statusbar.total_frames_label)

        self.statusbar.duration_label = Qt.QLabel(self.central_widget)
        self.statusbar.duration_label.setObjectName(
            'MainWindow.statusbar.duration_label')
        self.statusbar.addWidget(self.statusbar.duration_label)

        self.statusbar.resolution_label = Qt.QLabel(self.central_widget)
        self.statusbar.resolution_label.setObjectName(
            'MainWindow.statusbar.resolution_label')
        self.statusbar.addWidget(self.statusbar.resolution_label)

        self.statusbar.pixel_format_label = Qt.QLabel(self.central_widget)
        self.statusbar.pixel_format_label.setObjectName(
            'MainWindow.statusbar.pixel_format_label')
        self.statusbar.addWidget(self.statusbar.pixel_format_label)

        self.statusbar.fps_label = Qt.QLabel(self.central_widget)
        self.statusbar.fps_label.setObjectName(
            'MainWindow.statusbar.fps_label')
        self.statusbar.addWidget(self.statusbar.fps_label)

        self.statusbar.label = Qt.QLabel(self.central_widget)
        self.statusbar.label.setObjectName('MainWindow.statusbar.label')
        self.statusbar.addPermanentWidget(self.statusbar.label)

        self.setStatusBar(self.statusbar)

        # dialogs

        self.script_error_dialog = ScriptErrorDialog(self)

    def patch_dark_stylesheet(self, stylesheet: str) -> str:
        return stylesheet + 'QGraphicsView { border: 0px; padding: 0px; }'

    def load_script(self, script_path: Path, external_args: str = '') -> None:
        from traceback import print_exc
        import shlex

        self.toolbars.playback.stop()

        self.statusbar.label.setText('Evaluating')
        self.script_path = script_path
        sys.path.append(str(self.script_path.parent))

        # Rewrite args so external args will be forwarded correctly
        if external_args:
            self.external_args = shlex.split(external_args)
        try:
            argv_orig = sys.argv
            sys.argv = [sys.argv[1]] + self.external_args
        except AttributeError:
            pass

        try:
            exec(self.script_path.read_text(), {})  # pylint: disable=exec-used
        except Exception:  # pylint: disable=broad-except
            logging.error(
                'Script contains error(s). Check following lines for details.')
            self.handle_script_error(
                'Script contains error(s). See console output for details.')
            print_exc()
            return
        finally:
            sys.argv = argv_orig
            sys.path.pop()

        if len(vs.get_outputs()) == 0:
            logging.error('Script has no outputs set.')
            self.handle_script_error('Script has no outputs set.')
            return

        self.toolbars.main.rescan_outputs()
        # self.init_outputs()
        self.switch_output(self.OUTPUT_INDEX)

        self.load_storage()

    def load_storage(self) -> None:
        import yaml

        storage_path = self.script_path.with_suffix('.yml')
        if storage_path.exists():
            try:
                yaml.load(storage_path.open(), Loader=yaml.Loader)
            except yaml.YAMLError as exc:
                if isinstance(exc, yaml.MarkedYAMLError):
                    logging.warning(
                        'Storage parsing failed on line {} column {}. Using defaults.'
                        .format(exc.problem_mark.line + 1,
                                exc.problem_mark.column + 1))  # pylint: disable=no-member
                else:
                    logging.warning('Storage parsing failed. Using defaults.')
        else:
            logging.info('No storage found. Using defaults.')

        self.statusbar.label.setText('Ready')

    def init_outputs(self) -> None:
        self.graphics_scene.clear()
        for output in self.outputs:
            frame_pixmap = output.render_frame(output.last_showed_frame)
            frame_item = self.graphics_scene.addPixmap(frame_pixmap)
            frame_item.hide()
            output.graphics_scene_item = frame_item

    def reload_script(self) -> None:
        if self.toolbars.misc.autosave_enabled:
            self.toolbars.misc.save()
        vs.clear_outputs()
        self.graphics_scene.clear()
        self.load_script(self.script_path)

        self.show_message('Reloaded successfully')

    def render_frame(self,
                     frame: Frame,
                     output: Optional[Output] = None) -> Qt.QPixmap:
        return self.current_output.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.setPixmap(
                self.render_frame(frame))

    def switch_output(self, value: Union[int, Output]) -> None:
        if len(self.outputs) == 0:
            return
        if isinstance(value, Output):
            index = self.outputs.index_of(value)
        else:
            index = value

        if index < 0 or index >= len(self.outputs):
            return

        prev_index = self.toolbars.main.outputs_combobox.currentIndex()

        self.toolbars.playback.stop()

        # current_output relies on outputs_combobox
        self.toolbars.main.on_current_output_changed(index, prev_index)
        self.timeline.set_end_frame(self.current_output.end_frame)
        if self.current_output.frame_to_show is not None:
            self.current_frame = self.current_output.frame_to_show
        else:
            self.current_frame = self.current_output.last_showed_frame

        for output in self.outputs:
            output.graphics_scene_item.hide()
        self.current_output.graphics_scene_item.show()
        self.graphics_scene.setSceneRect(
            Qt.QRectF(self.current_output.graphics_scene_item.pixmap().rect()))
        self.timeline.update_notches()
        for toolbar in self.toolbars:
            toolbar.on_current_output_changed(index, prev_index)
        self.update_statusbar_output_info()

    @property  # type: ignore
    def current_output(self) -> Output:  # type: ignore
        output = cast(Output,
                      self.toolbars.main.outputs_combobox.currentData())
        return output

    @current_output.setter
    def current_output(self, value: Output) -> None:
        self.switch_output(self.outputs.index_of(value))

    @property  # type: ignore
    def current_frame(self) -> Frame:  # type: ignore
        return self.current_output.last_showed_frame

    @current_frame.setter
    def current_frame(self, value: Frame) -> None:
        self.switch_frame(value)

    @property  # type: ignore
    def current_time(self) -> Time:  # type: ignore
        return Time(self.current_output.last_showed_frame)

    @current_time.setter
    def current_time(self, value: Time) -> None:
        self.switch_frame(time=value)

    @property
    def outputs(self) -> Outputs:  # type: ignore
        return cast(Outputs, self.toolbars.main.outputs)

    def handle_script_error(self, message: str) -> None:
        # logging.error(message)
        self.script_error_dialog.label.setText(message)
        self.script_error_dialog.open()

    def on_wheel_scrolled(self, steps: int) -> None:
        new_index = self.toolbars.main.zoom_combobox.currentIndex() + steps
        if new_index < 0:
            new_index = 0
        elif new_index >= len(self.toolbars.main.zoom_levels):
            new_index = len(self.toolbars.main.zoom_levels) - 1
        self.toolbars.main.zoom_combobox.setCurrentIndex(new_index)

    def show_message(self,
                     message: str,
                     timeout: Optional[int] = None) -> None:
        if timeout is None:
            timeout = self.STATUSBAR_MESSAGE_TIMEOUT
        self.statusbar.showMessage(message, timeout)

    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('{} '.format(output.total_time))
        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 event(self, event: Qt.QEvent) -> bool:
        if event.type() == Qt.QEvent.LayoutRequest:
            self.timeline.full_repaint()

        return super().event(event)

    # misc methods

    def showEvent(self, event: Qt.QShowEvent) -> None:
        super().showEvent(event)
        self.graphics_view.setSizePolicy(
            Qt.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding))

    def closeEvent(self, event: Qt.QCloseEvent) -> None:
        if (self.toolbars.misc.autosave_enabled and self.save_on_exit):
            self.toolbars.misc.save()

    def __getstate__(self) -> Mapping[str, Any]:
        state = {
            attr_name: getattr(self, attr_name)
            for attr_name in self.storable_attrs
        }
        state.update({'timeline_mode': self.timeline.mode})
        return state

    def __setstate__(self, state: Mapping[str, Any]) -> None:
        # toolbars is singleton,
        # so it initialize itself right in its __setstate__()

        try:
            timeline_mode = state['timeline_mode']
            if not Timeline.Mode.is_valid(timeline_mode):
                raise TypeError
        except (KeyError, TypeError):
            logging.warning(
                'Storage loading: failed to parse timeline mode. Using default.'
            )
            timeline_mode = self.TIMELINE_MODE
        self.timeline.mode = timeline_mode