Esempio n. 1
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. 2
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. 3
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