Skip to content

Developer API Overview

This section covers the APIs available for developing plugins and extending VSView.

Most of these symbols are re-exported through the top-level vsview.api module for convenience.


Core & Proxies

Custom Widgets

UI Settings

Timeline & Playback

Utilities


Core & Proxies

Classes

PluginAPI

PluginAPI(workspace: LoaderWorkspace[Any])

Bases: _PluginAPI

API for plugins to interact with the workspace.

Source code in src/vsview/app/plugins/_interface.py
def __init__(self, workspace: LoaderWorkspace[Any]) -> None:
    super().__init__()
    self.__workspace = workspace
    self.__settings_store: _PluginSettingsStore | None = None
    self.__busy_callers = QObjectCounter[QObject]()

    SettingsManager.signals.globalChanged.connect(self._on_global_settings_changed)
    SettingsManager.signals.localChanged.connect(self._on_local_settings_changed)
    SettingsManager.signals.aboutToSaveGlobal.connect(self.aboutToSaveGlobal.emit)
    SettingsManager.signals.aboutToSaveLocal.connect(self.aboutToSaveLocal.emit)
Attributes
statusMessage
statusMessage = Signal(str)

Signal to emit status messages.

globalSettingsChanged
globalSettingsChanged = Signal()

Signal to emit when global settings change.

localSettingsChanged
localSettingsChanged = Signal(str)

Signal to emit when local settings change.

aboutToSaveGlobal
aboutToSaveGlobal = Signal()

Signal to emit before global settings are saved.

aboutToSaveLocal
aboutToSaveLocal = Signal(str)

Signal to emit before local settings are saved.

file_path
file_path: Path | None

Return the file path of the currently loaded file, or None if not a file.

current_frame
current_frame: Frame

Return the current frame number.

current_time
current_time: Time

Return the current time.

current_video_index
current_video_index: int

Return the index of the currently selected tab.

voutputs

Return a dictionary of VideoOutputProxy objects for all tabs.

current_voutput
current_voutput: VideoOutputProxy

Return the VideoOutput for the currently selected tab.

aoutputs

Return a list of AudioOutputProxy objects.

is_playing
is_playing: bool

Return whether playback is currently active.

packer
packer: Packer

Return the packer used by the workspace.

settings
settings: SimpleNamespace

Unstable API

Return the application's global and local settings as nested SimpleNamespace proxy objects.

current_view
current_view: GraphicsViewProxy

Return a proxy for the current view.

timeline
timeline: TimelineProxy

Return a proxy for the timeline.

playback
playback: PlaybackProxy

Return a proxy for the playback.

busy
busy: bool

Return whether the plugin API is busy by any plugins.

Functions
get_local_storage
get_local_storage(plugin: _PluginBase[Any, Any]) -> Path | None

Return a path to a local storage directory for the given plugin, or None if the current workspace has no file path.

Source code in src/vsview/app/plugins/api.py
def get_local_storage(self, plugin: _PluginBase[Any, Any]) -> Path | None:
    """
    Return a path to a local storage directory for the given plugin,
    or None if the current workspace has no file path.
    """
    if not self.file_path:
        return None

    settings_path = SettingsManager.local_settings_path(self.file_path)
    local_storage = settings_path.with_suffix("").with_stem(settings_path.stem.upper()) / plugin.identifier
    local_storage.mkdir(parents=True, exist_ok=True)

    return local_storage
register_on_destroy
register_on_destroy(cb: Callable[[], Any]) -> None

Register a callback to be called before the workspace begins a reload or when the workspace is destroyed. This is generaly used to clean up VapourSynth resources.

Source code in src/vsview/app/plugins/api.py
def register_on_destroy(self, cb: Callable[[], Any]) -> None:
    """
    Register a callback to be called before the workspace begins a reload or when the workspace is destroyed.
    This is generaly used to clean up VapourSynth resources.
    """
    self.__workspace.cbs_on_destroy.append(cb)
vs_context
vs_context() -> Iterator[None]

Context manager for using the VapourSynth environment of the workspace.

Source code in src/vsview/app/plugins/api.py
@contextmanager
def vs_context(self) -> Iterator[None]:
    """
    Context manager for using the VapourSynth environment of the workspace.
    """
    with self.__workspace.env.use():
        yield
block_workspace
block_workspace(caller: WidgetPluginBase[Any, Any]) -> Iterator[None]

Mark the workspace as busy for the duration of the context.

Specifically, a busy workspace will prevent:

  • Automatic or manual reloading.
  • Frame requests (via seeking, programmatic calls, or playback start).
Source code in src/vsview/app/plugins/api.py
@contextmanager
def block_workspace(self, caller: WidgetPluginBase[Any, Any]) -> Iterator[None]:
    """
    Mark the workspace as busy for the duration of the context.

    Specifically, a busy workspace will prevent:

    * Automatic or manual reloading.
    * Frame requests (via seeking, programmatic calls, or playback start).
    """
    self.__busy_callers.add(caller)

    try:
        yield
    finally:
        self.__busy_callers.discard(caller)
register_action
register_action(
    action_id: str,
    action: QAction,
    *,
    context: ShortcutContext = WidgetWithChildrenShortcut,
) -> None

Register a QAction for shortcut management.

Parameters:

Name Type Description Default
action_id str

The namespaced identifier (e.g., "my_plugin.do_thing").

required
action QAction

The QAction to manage.

required
context ShortcutContext

The context in which the shortcut should be active.

WidgetWithChildrenShortcut
Source code in src/vsview/app/plugins/api.py
def register_action(
    self,
    action_id: str,
    action: QAction,
    *,
    context: Qt.ShortcutContext = Qt.ShortcutContext.WidgetWithChildrenShortcut,
) -> None:
    """
    Register a QAction for shortcut management.

    Args:
        action_id: The namespaced identifier (e.g., "my_plugin.do_thing").
        action: The QAction to manage.
        context: The context in which the shortcut should be active.
    """
    ShortcutManager.register_action(action_id, action, context=context)
register_shortcut
register_shortcut(
    action_id: str,
    callback: Callable[[], Any],
    parent: QWidget,
    *,
    context: ShortcutContext = WidgetWithChildrenShortcut,
) -> QShortcut

Create and register a QShortcut for shortcut management.

Parameters:

Name Type Description Default
action_id str

The namespaced identifier (e.g., "my_plugin.do_thing").

required
callback Callable[[], Any]

The function to call when the shortcut is activated.

required
parent QWidget

The parent widget that determines shortcut scope.

required
context ShortcutContext

The context in which the shortcut should be active.

WidgetWithChildrenShortcut

Returns:

Type Description
QShortcut

The created QShortcut instance.

Source code in src/vsview/app/plugins/api.py
def register_shortcut(
    self,
    action_id: str,
    callback: Callable[[], Any],
    parent: QWidget,
    *,
    context: Qt.ShortcutContext = Qt.ShortcutContext.WidgetWithChildrenShortcut,
) -> QShortcut:
    """
    Create and register a QShortcut for shortcut management.

    Args:
        action_id: The namespaced identifier (e.g., "my_plugin.do_thing").
        callback: The function to call when the shortcut is activated.
        parent: The parent widget that determines shortcut scope.
        context: The context in which the shortcut should be active.

    Returns:
        The created QShortcut instance.
    """
    return ShortcutManager.register_shortcut(action_id, callback, parent, context=context)
get_shortcut_label
get_shortcut_label(action_id: str) -> str

Return the current shortcut's native display string or an empty string if no shortcut is assigned.

Source code in src/vsview/app/plugins/api.py
def get_shortcut_label(self, action_id: str) -> str:
    """
    Return the current shortcut's native display string or an empty string if no shortcut is assigned.
    """
    key = ShortcutManager.get_key(action_id)
    return QKeySequence(key).toString(QKeySequence.SequenceFormat.NativeText) if key else ""

VideoOutputProxy

VideoOutputProxy(
    vs_index: int,
    vs_name: str,
    vs_output: VideoOutputTuple,
    props: Mapping[int, Mapping[str, Any]],
    framedurs: Sequence[float] | None,
    cum_durations: Sequence[float] | None,
    kwargs: Mapping[str, Any],
    info: OutputInfo,
)

Read-only proxy for a video output.

Attributes
vs_index
vs_index: int

Index of the video output in the VapourSynth environment.

vs_name
vs_name: str = field(hash=False, compare=False)

Name of the video output.

vs_output
vs_output: VideoOutputTuple = field(hash=False, compare=False)

The object created by vapoursynth.get_outputs().

props
props: Mapping[int, Mapping[str, Any]] = field(hash=False, compare=False)

Frame properties of the clip.

framedurs
framedurs: Sequence[float] | None = field(hash=False, compare=False)

Frame durations of the clip.

cum_durations
cum_durations: Sequence[float] | None = field(hash=False, compare=False)

Cumulative durations of the clip.

kwargs
kwargs: Mapping[str, Any] = field(hash=False, compare=False)

Additional metadata provided by the user via set_output().

info
info: OutputInfo = field(hash=False, compare=False)

Output information.

Functions
time_to_frame
time_to_frame(time: timedelta, fps: VideoOutputProxy | Fraction | None = None) -> Frame

Convert a time to a frame number for this output.

Parameters:

Name Type Description Default
time timedelta

The time to convert.

required
fps VideoOutputProxy | Fraction | None

Optional override for FPS/duration context.

None
Source code in src/vsview/app/plugins/api.py
def time_to_frame(self, time: timedelta, fps: VideoOutputProxy | Fraction | None = None) -> Frame:
    """
    Convert a time to a frame number for this output.

    Args:
        time: The time to convert.
        fps: Optional override for FPS/duration context.
    """
    return VideoOutput.time_to_frame(self, time, fps)  # type: ignore[arg-type]
frame_to_time
frame_to_time(frame: int, fps: VideoOutputProxy | Fraction | None = None) -> Time

Convert a frame number to time for this output.

Parameters:

Name Type Description Default
frame int

The frame number to convert.

required
fps VideoOutputProxy | Fraction | None

Optional override for FPS/duration context.

None
Source code in src/vsview/app/plugins/api.py
def frame_to_time(self, frame: int, fps: VideoOutputProxy | Fraction | None = None) -> Time:
    """
    Convert a frame number to time for this output.

    Args:
        frame: The frame number to convert.
        fps: Optional override for FPS/duration context.
    """
    return VideoOutput.frame_to_time(self, frame, fps)  # type: ignore[arg-type]

AudioOutputProxy

AudioOutputProxy(
    vs_index: int, vs_name: str, vs_output: AudioNode, kwargs: Mapping[str, Any]
)

Read-only proxy for an audio output.

Attributes
vs_index
vs_index: int

Index of the audio output in the VapourSynth environment.

vs_name
vs_name: str = field(hash=False, compare=False)

Name of the audio output

vs_output
vs_output: AudioNode = field(hash=False, compare=False)

The object created by vapoursynth.get_outputs().

kwargs
kwargs: Mapping[str, Any] = field(hash=False, compare=False)

Additional metadata provided by the user via set_output().

GraphicsViewProxy

GraphicsViewProxy(workspace: LoaderWorkspace[Any], view: GraphicsView)

Bases: _GraphicsViewProxy

Proxy for a graphics view.

Source code in src/vsview/app/plugins/_interface.py
def __init__(self, workspace: LoaderWorkspace[Any], view: GraphicsView) -> None:
    super().__init__()
    self.__workspace = workspace
    self.__view = view
Attributes
pixmap
pixmap: QPixmap

Return the pixmap (implicitly shared).

image
image: QImage

Return a copy of the image.

rect_selection_enabled
rect_selection_enabled: bool

Return whether rectangular selection editing is enabled.

rect_selection
rect_selection: QRect

Return the current rectangular selection in source image pixel coordinates.

viewport
viewport: ViewportProxy

Return a proxy for the viewport.

Classes
ViewportProxy
ViewportProxy(workspace: LoaderWorkspace[Any], viewport: QWidget)

Bases: _ViewportProxy

Proxy for a viewport.

Source code in src/vsview/app/plugins/_interface.py
def __init__(self, workspace: LoaderWorkspace[Any], viewport: QWidget) -> None:
    super().__init__()
    self.__workspace = workspace
    self.__viewport = viewport
    self.__cursor_reset_conn: QMetaObject.Connection | None = None
Functions
map_from_global
map_from_global(*args: Any, **kwargs: Any) -> Any

Map global coordinates to the current view's local coordinates.

Source code in src/vsview/app/plugins/api.py
@copy_signature(QWidget().mapFromGlobal if TYPE_CHECKING else lambda *args, **kwargs: cast(Any, None))
def map_from_global(self, *args: Any, **kwargs: Any) -> Any:
    """
    Map global coordinates to the current view's local coordinates.
    """
    return self.__viewport.mapFromGlobal(*args, **kwargs)
set_cursor
set_cursor(cursor: QCursor | CursorShape) -> None

Set the cursor for the current view's viewport.

Source code in src/vsview/app/plugins/api.py
def set_cursor(self, cursor: QCursor | Qt.CursorShape) -> None:
    """
    Set the cursor for the current view's viewport.
    """
    v = self.__viewport
    v.setCursor(cursor)

    if self.__cursor_reset_conn:
        self.__workspace.tab_manager.tabChanged.disconnect(self.__cursor_reset_conn)

    def reset_cursor() -> None:
        if Shiboken.isValid(v):
            v.setCursor(Qt.CursorShape.OpenHandCursor)
        self.__cursor_reset_conn = None

    self.__cursor_reset_conn = self.__workspace.tab_manager.tabChanged.connect(
        reset_cursor,
        Qt.ConnectionType.SingleShotConnection,  # Auto-disconnects after first emit
    )
Functions
set_rect_selection
set_rect_selection(rect: QRect, *, finished: bool = False) -> None

Set the current rectangular selection in source image pixel coordinates.

Source code in src/vsview/app/plugins/api.py
def set_rect_selection(self, rect: QRect, *, finished: bool = False) -> None:
    """Set the current rectangular selection in source image pixel coordinates."""
    self.__view.set_rect_selection(rect, finished=finished)
clear_rect_selection
clear_rect_selection() -> None

Clear the current rectangular selection.

Source code in src/vsview/app/plugins/api.py
def clear_rect_selection(self) -> None:
    """Clear the current rectangular selection."""
    self.__view.clear_rect_selection()
map_to_scene
map_to_scene(*args: Any, **kwargs: Any) -> Any

Map coordinates to this view's scene.

Source code in src/vsview/app/plugins/api.py
@copy_signature(QGraphicsView().mapToScene if TYPE_CHECKING else lambda *args, **kwargs: cast(Any, None))
def map_to_scene(self, *args: Any, **kwargs: Any) -> Any:
    """
    Map coordinates to this view's scene.
    """
    return self.__view.mapToScene(*args, **kwargs)
map_from_scene
map_from_scene(*args: Any, **kwargs: Any) -> Any

Map coordinates from view's scene.

Source code in src/vsview/app/plugins/api.py
@copy_signature(QGraphicsView().mapFromScene if TYPE_CHECKING else lambda *args, **kwargs: cast(Any, None))
def map_from_scene(self, *args: Any, **kwargs: Any) -> Any:
    """
    Map coordinates from view's scene.
    """
    return self.__view.mapFromScene(*args, **kwargs)

PluginSettings

PluginSettings(plugin: _PluginBase[TGlobalSettings, TLocalSettings])

Bases: Generic[TGlobalSettings, TLocalSettings]

Settings wrapper providing lazy, always-fresh access.

Returns None if no settings model is defined for the scope.

Source code in src/vsview/app/plugins/api.py
def __init__(self, plugin: _PluginBase[TGlobalSettings, TLocalSettings]) -> None:
    self._plugin = plugin
Attributes
global_
global_: TGlobalSettings

Get the current global settings.

local_
local_: TLocalSettings

Get the current local settings (resolved with global fallbacks).

WidgetPluginBase

WidgetPluginBase(parent: QWidget, api: PluginAPI)

Bases: _PluginBase[TGlobalSettings, TLocalSettings], QWidget

Base class for all widget plugins.

Source code in src/vsview/app/plugins/api.py
def __init__(self, parent: QWidget, api: PluginAPI) -> None:
    QWidget.__init__(self, parent)
    self.api = api
    self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
Attributes
identifier
identifier: str

Unique identifier for the plugin.

display_name
display_name: str

Display name for the plugin.

shortcuts
shortcuts: Sequence[ActionDefinition] = ()

Keyboard shortcuts for this plugin.

Each ActionDefinition ID must start with "{identifier}." prefix.

settings
settings: PluginSettings[TGlobalSettings, TLocalSettings]

Get the settings wrapper for lazy, always-fresh access.

secrets
secrets: PluginSecrets

Get a namespaced secure secrets API for this plugin.

Functions
update_global_settings
update_global_settings(**updates: Any) -> None

Update specific global settings fields and trigger persistence.

Source code in src/vsview/app/plugins/api.py
def update_global_settings(self, **updates: Any) -> None:
    """Update specific global settings fields and trigger persistence."""
    self.api._update_settings(self, "global", **updates)
update_local_settings
update_local_settings(**updates: Any) -> None

Update specific local settings fields and trigger persistence.

Source code in src/vsview/app/plugins/api.py
def update_local_settings(self, **updates: Any) -> None:
    """Update specific local settings fields and trigger persistence."""
    self.api._update_settings(self, "local", **updates)
on_current_voutput_changed
on_current_voutput_changed(voutput: VideoOutputProxy, tab_index: int) -> None

Called when the current video output changes.

Execution Thread: Main or Background. If you need to update the UI, use the @run_in_loop decorator.

Source code in src/vsview/app/plugins/api.py
def on_current_voutput_changed(self, voutput: VideoOutputProxy, tab_index: int) -> None:
    """
    Called when the current video output changes.

    Execution Thread: **Main or Background**.
    If you need to update the UI, use the `@run_in_loop` decorator.
    """
    self.on_current_frame_changed(self.api.current_frame)
on_current_frame_changed
on_current_frame_changed(n: int) -> None

Called when the current frame changes.

Execution Thread: Main or Background. If you need to update the UI, use the @run_in_loop decorator.

Source code in src/vsview/app/plugins/api.py
def on_current_frame_changed(self, n: int) -> None:
    """
    Called when the current frame changes.

    Execution Thread: **Main or Background**.
    If you need to update the UI, use the `@run_in_loop` decorator.
    """
on_playback_started
on_playback_started() -> None

Called when playback starts.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_playback_started(self) -> None:
    """
    Called when playback starts.

    Execution Thread: **Main**.
    """
on_playback_stopped
on_playback_stopped() -> None

Called when playback stops.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_playback_stopped(self) -> None:
    """
    Called when playback stops.

    Execution Thread: **Main**.
    """
on_view_context_menu
on_view_context_menu(event: QContextMenuEvent) -> None

Called when a context menu of the current viewis requested.

The event is forwarded BEFORE the view processes it.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_view_context_menu(self, event: QContextMenuEvent) -> None:
    """
    Called when a context menu of the current viewis requested.

    The event is forwarded BEFORE the view processes it.

    Execution Thread: **Main**.
    """
on_view_mouse_moved
on_view_mouse_moved(event: QMouseEvent) -> None

Called when the mouse of the current view is moved.

The event is forwarded AFTER the view processes it.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_view_mouse_moved(self, event: QMouseEvent) -> None:
    """
    Called when the mouse of the current view is moved.

    The event is forwarded AFTER the view processes it.

    Execution Thread: **Main**.
    """
on_view_mouse_pressed
on_view_mouse_pressed(event: QMouseEvent) -> None

Called when the mouse of the current view is pressed.

The event is forwarded AFTER the view processes it.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_view_mouse_pressed(self, event: QMouseEvent) -> None:
    """
    Called when the mouse of the current view is pressed.

    The event is forwarded AFTER the view processes it.

    Execution Thread: **Main**.
    """
on_view_mouse_released
on_view_mouse_released(event: QMouseEvent) -> None

Called when the mouse of the current view is released.

The event is forwarded AFTER the view processes it.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_view_mouse_released(self, event: QMouseEvent) -> None:
    """
    Called when the mouse of the current view is released.

    The event is forwarded AFTER the view processes it.

    Execution Thread: **Main**.
    """
on_view_rect_selection_changed
on_view_rect_selection_changed(rect: QRect) -> None

Called when the current view's rectangular selection changes.

rect is in source image pixel coordinates. An empty rect means the selection was cleared.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_view_rect_selection_changed(self, rect: QRect) -> None:
    """
    Called when the current view's rectangular selection changes.

    `rect` is in source image pixel coordinates. An empty rect means the selection was cleared.

    Execution Thread: **Main**.
    """
on_view_rect_selection_finished
on_view_rect_selection_finished(rect: QRect) -> None

Called when the current view's rectangular selection interaction finishes.

rect is in source image pixel coordinates. An empty rect means the selection was cleared.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_view_rect_selection_finished(self, rect: QRect) -> None:
    """
    Called when the current view's rectangular selection interaction finishes.

    `rect` is in source image pixel coordinates. An empty rect means the selection was cleared.

    Execution Thread: **Main**.
    """
on_view_key_press
on_view_key_press(event: QKeyEvent) -> None

Called when a key is pressed in the current view.

The event is forwarded AFTER the view processes it.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_view_key_press(self, event: QKeyEvent) -> None:
    """
    Called when a key is pressed in the current view.

    The event is forwarded AFTER the view processes it.

    Execution Thread: **Main**.
    """
on_view_key_release
on_view_key_release(event: QKeyEvent) -> None

Called when a key is released in the current view.

The event is forwarded AFTER the view processes it.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_view_key_release(self, event: QKeyEvent) -> None:
    """
    Called when a key is released in the current view.

    The event is forwarded AFTER the view processes it.

    Execution Thread: **Main**.
    """
on_hide
on_hide() -> None

Called when the plugin is hidden.

Execution Thread: Main.

Source code in src/vsview/app/plugins/api.py
def on_hide(self) -> None:
    """
    Called when the plugin is hidden.

    Execution Thread: **Main**.
    """

NodeProcessor

NodeProcessor(api: PluginAPI)

Bases: _PluginBase[TGlobalSettings, TLocalSettings], Generic[NodeT, TGlobalSettings, TLocalSettings]

Interface for objects that process VapourSynth nodes.

Source code in src/vsview/app/plugins/api.py
def __init__(self, api: PluginAPI, /) -> None:
    self.api = api
Attributes
identifier
identifier: str

Unique identifier for the plugin.

display_name
display_name: str

Display name for the plugin.

shortcuts
shortcuts: Sequence[ActionDefinition] = ()

Keyboard shortcuts for this plugin.

Each ActionDefinition ID must start with "{identifier}." prefix.

settings
settings: PluginSettings[TGlobalSettings, TLocalSettings]

Get the settings wrapper for lazy, always-fresh access.

secrets
secrets: PluginSecrets

Get a namespaced secure secrets API for this plugin.

Functions
update_global_settings
update_global_settings(**updates: Any) -> None

Update specific global settings fields and trigger persistence.

Source code in src/vsview/app/plugins/api.py
def update_global_settings(self, **updates: Any) -> None:
    """Update specific global settings fields and trigger persistence."""
    self.api._update_settings(self, "global", **updates)
update_local_settings
update_local_settings(**updates: Any) -> None

Update specific local settings fields and trigger persistence.

Source code in src/vsview/app/plugins/api.py
def update_local_settings(self, **updates: Any) -> None:
    """Update specific local settings fields and trigger persistence."""
    self.api._update_settings(self, "local", **updates)
prepare
prepare(node: NodeT) -> NodeT

Process the input node and return a modified node of the same type.

Parameters:

Name Type Description Default
node NodeT

The raw input node (VideoNode or AudioNode).

required

Returns:

Type Description
NodeT

The processed node compatible with the player's output requirements.

Source code in src/vsview/app/plugins/api.py
def prepare(self, node: NodeT, /) -> NodeT:
    """
    Process the input node and return a modified node of the same type.

    Args:
        node: The raw input node (VideoNode or AudioNode).

    Returns:
        The processed node compatible with the player's output requirements.
    """
    raise NotImplementedError

ActionDefinition

Bases: str

Unified definition and identifier for a shortcut action.

Attributes
label
label: str

Human-readable display label

default_key
default_key: str

Default key sequence (can be empty)

LocalSettingsModel

Bases: BaseModel

Base class for settings with optional local overrides.

Fields set to None fall back to the corresponding global value.

Functions
resolve
resolve(global_settings: BaseModel) -> Self

Resolve global settings with local overrides applied.

Parameters:

Name Type Description Default
global_settings BaseModel

Source of default values.

required

Returns:

Type Description
Self

A new instance with all fields resolved.

Source code in src/vsview/app/plugins/api.py
def resolve(self, global_settings: BaseModel) -> Self:
    """
    Resolve global settings with local overrides applied.

    Args:
        global_settings: Source of default values.

    Returns:
        A new instance with all fields resolved.
    """
    base_values = global_settings.model_dump(include=set(self.__class__.model_fields))

    overrides = self.model_dump(exclude_none=True)

    return self.__class__(**base_values | overrides)

Custom Widgets

Classes

Accordion

Accordion(title: str, parent: QWidget | None = None, collapsed: bool = False)

Bases: QFrame

Source code in src/vsview/app/views/components.py
def __init__(self, title: str, parent: QWidget | None = None, collapsed: bool = False) -> None:
    super().__init__(parent)

    self.setFrameShape(QFrame.Shape.StyledPanel)
    self.setFrameShadow(QFrame.Shadow.Raised)

    self.main_layout = QVBoxLayout(self)
    self.main_layout.setContentsMargins(0, 0, 0, 0)
    self.main_layout.setSpacing(0)

    self.header = QToolButton(self)
    self.header.setText(f"  {title}")
    self.header.setCheckable(True)
    self.header.setChecked(not collapsed)
    self.header.setArrowType(Qt.ArrowType.DownArrow if not collapsed else Qt.ArrowType.RightArrow)
    self.header.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
    self.header.setSizePolicy(
        self.header.sizePolicy().horizontalPolicy(),
        self.header.sizePolicy().verticalPolicy(),
    )
    self.header.setStyleSheet(self.HEADER_STYLE)
    self.header.toggled.connect(self.on_toggle)
    self.main_layout.addWidget(self.header)

    self.content = QWidget(self)
    self.content_layout = QVBoxLayout(self.content)
    self.content_layout.setContentsMargins(12, 8, 12, 12)
    self.content_layout.setSpacing(8)
    self.main_layout.addWidget(self.content)

    self.animation = QPropertyAnimation(self.content, b"maximumHeight")
    # FIXME: A larger duration just seems to increase flickering and ghosting during collapsing
    self.animation.setDuration(80)
    self.animation.setEasingCurve(QEasingCurve.Type.Linear)

    if collapsed:
        self.content.setMaximumHeight(0)

AnimatedToggle

AnimatedToggle(parent: QWidget | None = None)

Bases: QCheckBox

Source code in src/vsview/app/views/components.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)

    palette = self.palette()

    self.bar = palette.color(QPalette.ColorRole.Light)
    self.bar_disabled = palette.color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Light).darker()
    self.bar_checked = palette.color(QPalette.ColorRole.Accent)
    self.bar_checked_disabled = palette.color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Accent)

    self.handle = palette.color(QPalette.ColorRole.Midlight)
    self.handle_disabled = palette.color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Midlight).darker(225)
    self.handle_checked = palette.color(QPalette.ColorRole.Highlight)
    self.handle_checked_disabled = palette.color(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Accent).darker()

    self.setContentsMargins(8, 0, 8, 0)
    self.handle_position = 0.0

    self.animation = QVariantAnimation(self)
    self.animation.setEasingCurve(QEasingCurve.Type.InOutCubic)
    self.animation.setDuration(150)
    self.animation.valueChanged.connect(self._on_value_changed)

    self.stateChanged.connect(self._setup_animation)

SegmentedControl

SegmentedControl(
    labels: Sequence[str],
    parent: QWidget | None = None,
    direction: Direction = LeftToRight,
)

Bases: QWidget

A segmented control widget for binary choices.

Emits segmentChanged signal with the index of the selected segment.

Source code in src/vsview/app/views/components.py
def __init__(
    self,
    labels: Sequence[str],
    parent: QWidget | None = None,
    direction: QBoxLayout.Direction = QBoxLayout.Direction.LeftToRight,
) -> None:
    super().__init__(parent)

    self.current_layout = QBoxLayout(direction, self)
    self.current_layout.setContentsMargins(2, 2, 2, 2)
    self.current_layout.setSpacing(2)

    self.button_group = QButtonGroup(self)
    self.button_group.setExclusive(True)
    self.buttons = list[QPushButton]()

    for i, label in enumerate(labels):
        btn = QPushButton(label, self)
        btn.setCheckable(True)
        btn.setCursor(Qt.CursorShape.PointingHandCursor)

        self.button_group.addButton(btn, i)
        self.buttons.append(btn)
        self.current_layout.addWidget(btn)

    if self.buttons:
        self.buttons[0].setChecked(True)

    self.button_group.idClicked.connect(self._on_button_clicked)
    self._update_button_colors()

FrameEdit

FrameEdit(parent: QWidget)

Bases: QSpinBox

Source code in src/vsview/app/views/timeline.py
def __init__(self, parent: QWidget) -> None:
    super().__init__(parent)
    self.valueChanged.connect(self._on_value_changed)

    self.setMinimum(0)
    self.setKeyboardTracking(False)

    self.old_value = self.value()

TimeEdit

TimeEdit(parent: QWidget)

Bases: QTimeEdit

Source code in src/vsview/app/views/timeline.py
def __init__(self, parent: QWidget) -> None:
    super().__init__(parent)
    self.timeChanged.connect(self._on_time_changed)

    self.setDisplayFormat("H:mm:ss.zzz")
    self.setButtonSymbols(QTimeEdit.ButtonSymbols.NoButtons)
    self.setMinimumTime(QTime())
    self.setKeyboardTracking(False)

    self.old_time = self.time()

BaseGraphicsView

BaseGraphicsView(*args: Any, **kwargs: Any)

Bases: QGraphicsView

Source code in src/vsview/app/views/video.py
@copy_signature(QGraphicsView.__init__)
def __init__(self, *args: Any, **kwargs: Any) -> None:
    super().__init__(*args, **kwargs)

    self.angle_remainder = 0
    self.current_zoom = 1.0
    self.autofit = False

    self._sar = 1.0
    self._sar_applied = False

    self.zoom_factors = SettingsManager.global_settings.view.zoom_factors.copy()
    SettingsManager.signals.globalChanged.connect(self._on_settings_changed)

    self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
    self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
    self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
    self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
    self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, False)

    self.graphics_scene = QGraphicsScene(self)

    self._checkerboard = self._create_checkerboard_pixmap()

    self.pixmap_item = self.graphics_scene.addPixmap(QPixmap())
    self.pixmap_item.setTransformationMode(Qt.TransformationMode.FastTransformation)
    self.setScene(self.graphics_scene)

    self._rect_selection = QRect()
    self._rect_selection_enabled = False
    self._rect_selection_drag: RectSelectionDragState | None = None
    self._init_rect_selection_overlay()

    self._zoom_animation = QVariantAnimation(self)
    self._zoom_animation.setDuration(125)
    self._zoom_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
    self._zoom_animation.valueChanged.connect(self._apply_zoom_value)

    self.wheelScrolled.connect(self._on_wheel_scrolled)

    self.context_menu = QMenu(self)

    self.slider_container = QWidget(self)
    self.slider = QSlider(Qt.Orientation.Horizontal, self.slider_container)
    self.slider.setRange(0, 100)
    self.slider.setValue(self._zoom_to_slider(1.0))
    self.slider.setMinimumWidth(100)
    self.slider.setToolTip("1.00x")
    self.slider.valueChanged.connect(self._on_slider_value_changed)

    self.slider_layout = QHBoxLayout(self.slider_container)
    self.slider_layout.addWidget(QLabel("Zoom", self.slider_container))
    self.slider_layout.addWidget(self.slider)

    self.slider_container.setLayout(self.slider_layout)

    self.slider_action = QWidgetAction(self.context_menu)
    self.slider_action.setDefaultWidget(self.slider_container)

    self.context_menu.addAction(self.slider_action)
    self.context_menu.addSeparator()

    self.autofit_action = self.context_menu.addAction("Autofit")
    self.autofit_action.setCheckable(True)
    self.autofit_action.setChecked(self.autofit)
    self.autofit_action.triggered.connect(self._on_autofit_action)

    self.apply_sar_action = self.context_menu.addAction("Toggle SAR")
    self.apply_sar_action.setCheckable(True)
    self.apply_sar_action.setChecked(self._sar_applied)
    self.apply_sar_action.setEnabled(False)  # Disabled until SAR != 1.0
    self.apply_sar_action.triggered.connect(self._set_sar_applied)

    self.save_image_action = self.context_menu.addAction("Save Current Image")
    self.save_image_action.triggered.connect(self._on_save_image_action)

    self.copy_image_action = self.context_menu.addAction("Copy Image to Clipboard")
    self.copy_image_action.triggered.connect(self._copy_image_to_clipboard)

    self._setup_shortcuts()
Attributes
rect_selection
rect_selection: QRect

Return the current rectangular selection in source image pixel coordinates (empty if cleared).

rect_selection_enabled
rect_selection_enabled: bool

Return whether rectangular selection overlay is active and editable.

Functions
map_to_image
map_to_image(point: QPoint | QPointF) -> QPointF

Map a point from view coordinates to the source image's pixel coordinate space.

Parameters:

Name Type Description Default
point QPoint | QPointF

The point in viewport or widget local coordinates.

required

Returns:

Type Description
QPointF

The corresponding point in the source image's pixel coordinates.

Source code in src/vsview/app/views/video.py
def map_to_image(self, point: QPoint | QPointF) -> QPointF:
    """
    Map a point from view coordinates to the source image's pixel coordinate space.

    Args:
        point: The point in viewport or widget local coordinates.

    Returns:
        The corresponding point in the source image's pixel coordinates.
    """
    point = point.toPoint() if isinstance(point, QPointF) else point
    item_pos = self.pixmap_item.mapFromScene(self.mapToScene(point))

    scale_x, scale_y = self._image_scale_factors

    return QPointF(item_pos.x() * scale_x, item_pos.y() * scale_y)
set_rect_selection
set_rect_selection(rect: QRect, *, finished: bool = False) -> None

Set the rectangular selection to the given QRect in source image coordinates.

Parameters:

Name Type Description Default
rect QRect

The bounding QRect, or an empty QRect to clear.

required
finished bool

If True, emits rectSelectionFinished signal as well.

False
Source code in src/vsview/app/views/video.py
def set_rect_selection(self, rect: QRect, *, finished: bool = False) -> None:
    """
    Set the rectangular selection to the given QRect in source image coordinates.

    Args:
        rect: The bounding QRect, or an empty QRect to clear.
        finished: If True, emits rectSelectionFinished signal as well.
    """
    self._set_rect_selection(rect, emit_changed=True, emit_finished=finished)
clear_rect_selection
clear_rect_selection() -> None

Clear the current rectangular selection and emit signals.

Source code in src/vsview/app/views/video.py
def clear_rect_selection(self) -> None:
    """Clear the current rectangular selection and emit signals."""
    self._set_rect_selection(QRect(), emit_changed=True, emit_finished=True)

PluginGraphicsView

PluginGraphicsView(parent: QWidget, api: PluginAPI)

Bases: BaseGraphicsView

Graphics view for plugins.

Source code in src/vsview/app/plugins/api.py
def __init__(self, parent: QWidget, api: PluginAPI) -> None:
    super().__init__(parent)
    self.api = api

    self.outputs = dict[int, vs.VideoNode]()
    self.current_tab = -1
    self.last_frame = -1

    self.api.register_on_destroy(self.outputs.clear)
Attributes
rect_selection
rect_selection: QRect

Return the current rectangular selection in source image pixel coordinates (empty if cleared).

rect_selection_enabled
rect_selection_enabled: bool

Return whether rectangular selection overlay is active and editable.

Functions
map_to_image
map_to_image(point: QPoint | QPointF) -> QPointF

Map a point from view coordinates to the source image's pixel coordinate space.

Parameters:

Name Type Description Default
point QPoint | QPointF

The point in viewport or widget local coordinates.

required

Returns:

Type Description
QPointF

The corresponding point in the source image's pixel coordinates.

Source code in src/vsview/app/views/video.py
def map_to_image(self, point: QPoint | QPointF) -> QPointF:
    """
    Map a point from view coordinates to the source image's pixel coordinate space.

    Args:
        point: The point in viewport or widget local coordinates.

    Returns:
        The corresponding point in the source image's pixel coordinates.
    """
    point = point.toPoint() if isinstance(point, QPointF) else point
    item_pos = self.pixmap_item.mapFromScene(self.mapToScene(point))

    scale_x, scale_y = self._image_scale_factors

    return QPointF(item_pos.x() * scale_x, item_pos.y() * scale_y)
set_rect_selection
set_rect_selection(rect: QRect, *, finished: bool = False) -> None

Set the rectangular selection to the given QRect in source image coordinates.

Parameters:

Name Type Description Default
rect QRect

The bounding QRect, or an empty QRect to clear.

required
finished bool

If True, emits rectSelectionFinished signal as well.

False
Source code in src/vsview/app/views/video.py
def set_rect_selection(self, rect: QRect, *, finished: bool = False) -> None:
    """
    Set the rectangular selection to the given QRect in source image coordinates.

    Args:
        rect: The bounding QRect, or an empty QRect to clear.
        finished: If True, emits rectSelectionFinished signal as well.
    """
    self._set_rect_selection(rect, emit_changed=True, emit_finished=finished)
clear_rect_selection
clear_rect_selection() -> None

Clear the current rectangular selection and emit signals.

Source code in src/vsview/app/views/video.py
def clear_rect_selection(self) -> None:
    """Clear the current rectangular selection and emit signals."""
    self._set_rect_selection(QRect(), emit_changed=True, emit_finished=True)
update_display
update_display(image: QImage) -> None

Update the UI with the new image on the main thread.

Source code in src/vsview/app/plugins/api.py
@run_in_loop(return_future=False)
def update_display(self, image: QImage) -> None:
    """Update the UI with the new image on the main thread."""
    self.set_pixmap(QPixmap.fromImage(image))
refresh
refresh() -> None

Refresh the view.

Source code in src/vsview/app/plugins/api.py
def refresh(self) -> None:
    """Refresh the view."""
    self.api._init_view(self, refresh=True)
on_current_voutput_changed
on_current_voutput_changed(voutput: VideoOutputProxy, tab_index: int) -> None

Called when the current video output changes.

Warning: Do not call self.refresh() here, as it will cause an infinite loop. If you need to update the display manually, use self.update_display().

Execution Thread: Main or Background. If you need to update the UI, use the @run_in_loop decorator.

Source code in src/vsview/app/plugins/api.py
def on_current_voutput_changed(self, voutput: VideoOutputProxy, tab_index: int) -> None:
    """
    Called when the current video output changes.

    **Warning**: Do not call `self.refresh()` here, as it will cause an infinite loop.
    If you need to update the display manually, use `self.update_display()`.

    Execution Thread: **Main or Background**.
    If you need to update the UI, use the `@run_in_loop` decorator.
    """
on_current_frame_changed
on_current_frame_changed(n: int, f: VideoFrame) -> None

Called when the current frame changes. n is the frame number and f is the packed VideoFrame in GRAY32 format.

Warning: Do not call self.refresh() here, as it will cause an infinite loop. If you need to update the display manually, use self.update_display().

Execution Thread: Main or Background. If you need to update the UI, use the @run_in_loop decorator.

Source code in src/vsview/app/plugins/api.py
def on_current_frame_changed(self, n: int, f: vs.VideoFrame) -> None:
    """
    Called when the current frame changes.
    `n` is the frame number and `f` is the packed VideoFrame in GRAY32 format.

    **Warning**: Do not call `self.refresh()` here, as it will cause an infinite loop.
    If you need to update the display manually, use `self.update_display()`.

    Execution Thread: **Main or Background**.
    If you need to update the UI, use the `@run_in_loop` decorator.
    """
    self.update_display(self.api.packer.frame_to_qimage(f).copy())
get_node
get_node(clip: VideoNode) -> VideoNode

Override this to transform the clip before it is displayed. By default, it returns the clip as-is.

Source code in src/vsview/app/plugins/api.py
def get_node(self, clip: vs.VideoNode) -> vs.VideoNode:
    """
    Override this to transform the clip before it is displayed.
    By default, it returns the clip as-is.
    """
    return clip

ListEditWidget

ListEditWidget(
    value_type: type[T],
    parent: QWidget | None = None,
    default_value: T | Sequence[T] | None = None,
    dialog_label_text: str | None = None,
    completions: Sequence[str] | None = None,
)

Bases: QWidget, IconReloadMixin

Structured list editor with Add/Remove buttons.

Source code in src/vsview/app/settings/models.py
def __init__(
    self,
    value_type: type[T],
    parent: QWidget | None = None,
    default_value: T | Sequence[T] | None = None,
    dialog_label_text: str | None = None,
    completions: Sequence[str] | None = None,
) -> None:
    super().__init__(parent)
    self.value_type = value_type
    self.adapter = TypeAdapter[T](value_type)
    self.default_value = default_value

    self.dialog = QInputDialog(self)
    self.dialog.setInputMode(QInputDialog.InputMode.TextInput)
    self.dialog.setWindowTitle("Add Item")
    self.dialog.setLabelText(dialog_label_text or f"Enter {self.value_type.__name__}:")
    self.dialog.finished.connect(self._on_dialog_finished)

    self.dialog_line_edit = self.dialog.findChild(QLineEdit)

    if completions and self.dialog_line_edit:
        self.completer: QCompleter | None = QCompleter(
            completions,
            self.dialog_line_edit,
            caseSensitivity=Qt.CaseSensitivity.CaseInsensitive,
        )
        self.dialog_line_edit.setCompleter(self.completer)
    else:
        self.completer = None

    self.setLayout(layout := QVBoxLayout(self))
    layout.setContentsMargins(0, 0, 0, 0)
    layout.setSpacing(4)

    self.list_widget = QListWidget(self)
    self.list_widget.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
    self.list_widget.setAlternatingRowColors(True)
    self.list_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding)
    self.list_widget.setMaximumHeight(100)
    layout.addWidget(self.list_widget)

    btn_layout = QHBoxLayout()
    btn_layout.setSpacing(4)

    self.add_btn = self.make_tool_button(IconName.PLUS, "Add Item", self)
    self.add_btn.clicked.connect(self.dialog.open)

    self.remove_btn = self.make_tool_button(IconName.MINUS, "Remove Item", self)
    self.remove_btn.clicked.connect(self._remove_selected)

    btn_layout.addWidget(self.add_btn)
    btn_layout.addWidget(self.remove_btn)
    btn_layout.addStretch()
    layout.addLayout(btn_layout)
Functions
register_icon_button
register_icon_button(
    button: QToolButton,
    icon_name: IconName,
    icon_size: QSize | None = None,
    color_role: ColorRole = ToolTipText,
    icon_states: Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]]
    | None = None,
) -> None

Register a button for automatic icon reload when settings change.

Parameters:

Name Type Description Default
button QToolButton

The QToolButton to update.

required
icon_name IconName

The IconName to use for the button.

required
icon_size QSize | None

Size for the icon.

None
color_role ColorRole

Palette color role for simple icons (used when icon_states is None).

ToolTipText
icon_states Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]] | None

Full mapping of (QIcon.Mode, QIcon.State) -> QPalette.ColorRole. Allows complete control over icon appearance for all Qt states.

Example:

{
    (QIcon.Mode.Normal, QIcon.State.Off): QPalette.ColorRole.ToolTipText,
    (QIcon.Mode.Normal, QIcon.State.On): QPalette.ColorRole.Highlight,
    (QIcon.Mode.Disabled, QIcon.State.Off): QPalette.ColorRole.Mid,
}

None
Source code in src/vsview/assets/utils.py
def register_icon_button(
    self,
    button: QToolButton,
    icon_name: IconName,
    icon_size: QSize | None = None,
    color_role: QPalette.ColorRole = QPalette.ColorRole.ToolTipText,
    icon_states: Mapping[
        tuple[QIcon.Mode, QIcon.State],
        QPalette.ColorRole | tuple[QPalette.ColorGroup, QPalette.ColorRole],
    ]
    | None = None,
) -> None:
    """
    Register a button for automatic icon reload when settings change.

    Args:
        button: The QToolButton to update.
        icon_name: The IconName to use for the button.
        icon_size: Size for the icon.
        color_role: Palette color role for simple icons (used when icon_states is None).
        icon_states: Full mapping of (QIcon.Mode, QIcon.State) -> QPalette.ColorRole.
            Allows complete control over icon appearance for all Qt states.

            Example:
                ```python
                {
                    (QIcon.Mode.Normal, QIcon.State.Off): QPalette.ColorRole.ToolTipText,
                    (QIcon.Mode.Normal, QIcon.State.On): QPalette.ColorRole.Highlight,
                    (QIcon.Mode.Disabled, QIcon.State.Off): QPalette.ColorRole.Mid,
                }
                ```
    """

    def reload(btn: QToolButton) -> None:
        palette = btn.palette()

        if icon_states:
            icon = self.make_icon(
                {
                    (mode, state): (
                        icon_name,
                        palette.color(*role) if isinstance(role, tuple) else palette.color(role),
                    )
                    for (mode, state), role in icon_states.items()
                },
                size=icon_size,
            )
        else:
            color = palette.color(QPalette.ColorGroup.Normal, color_role)
            icon = self.make_icon((icon_name, color), size=icon_size)
        btn.setIcon(icon)

    self._button_reloaders[button] = partial(reload, button)
register_icon_action
register_icon_action(
    action: QAction,
    icon_name: IconName,
    icon_size: QSize | None = None,
    color_role: ColorRole = ToolTipText,
    icon_states: Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]]
    | None = None,
) -> None

Register a QAction for automatic icon reload.

Source code in src/vsview/assets/utils.py
def register_icon_action(
    self,
    action: QAction,
    icon_name: IconName,
    icon_size: QSize | None = None,
    color_role: QPalette.ColorRole = QPalette.ColorRole.ToolTipText,
    icon_states: Mapping[
        tuple[QIcon.Mode, QIcon.State],
        QPalette.ColorRole | tuple[QPalette.ColorGroup, QPalette.ColorRole],
    ]
    | None = None,
) -> None:
    """Register a QAction for automatic icon reload."""

    def reload(act: QAction) -> None:
        # Action doesn't have a palette, so we use the parent widget's palette (self)
        palette = p.palette() if isinstance((p := action.parent()), QWidget) else getattr(self, "palette")()

        if icon_states:
            icon = self.make_icon(
                {
                    (mode, state): (
                        icon_name,
                        palette.color(*role) if isinstance(role, tuple) else palette.color(role),
                    )
                    for (mode, state), role in icon_states.items()
                },
                size=icon_size,
            )
        else:
            color = palette.color(QPalette.ColorGroup.Normal, color_role)
            icon = self.make_icon((icon_name, color), size=icon_size)
        act.setIcon(icon)

    self._action_reloaders[action] = partial(reload, action)
register_icon_callback
register_icon_callback(callback: Callable[[], None]) -> None

Register a custom callback for icon reloading.

Use this for complex icons that don't fit the standard button pattern (e.g., play/pause with different icons per state, spinner animations).

Parameters:

Name Type Description Default
callback Callable[[], None]

A callable that reloads the icon(s).

required
Source code in src/vsview/assets/utils.py
def register_icon_callback(self, callback: Callable[[], None]) -> None:
    """
    Register a custom callback for icon reloading.

    Use this for complex icons that don't fit the standard button pattern
    (e.g., play/pause with different icons per state, spinner animations).

    Args:
        callback: A callable that reloads the icon(s).
    """
    self._custom_callbacks.append(callback)
make_icon
make_icon(
    icons: tuple[IconName, QColor] | dict[tuple[Mode, State], tuple[IconName, QColor]],
    size: QSize | None = None,
) -> QIcon

Create a QIcon from either: - a single (IconName, color) tuple - or a dict mapping (mode, state) -> (IconName, color)

Parameters:

Name Type Description Default
icons tuple[IconName, QColor] | dict[tuple[Mode, State], tuple[IconName, QColor]]

Icon specification using IconName enum. Simple usage: (IconName.PLAY, QColor("white")) Full control:

```python
{
    (QIcon.Mode.Normal, QIcon.State.Off): (IconName.PLAY, QColor("white")),
    (QIcon.Mode.Normal, QIcon.State.On): (IconName.PAUSE, QColor("gray")),
    (QIcon.Mode.Disabled, QIcon.State.Off): (IconName.PLAY, QColor("darkgray")),
}
```
required
size QSize | None

Target size for SVG rendering (for crisp output).

None
Source code in src/vsview/assets/utils.py
@staticmethod
def make_icon(
    icons: tuple[IconName, QColor] | dict[tuple[QIcon.Mode, QIcon.State], tuple[IconName, QColor]],
    size: QSize | None = None,
) -> QIcon:
    """
    Create a QIcon from either:
    - a single (IconName, color) tuple
    - or a dict mapping (mode, state) -> (IconName, color)

    Args:
        icons: Icon specification using IconName enum.
            Simple usage: `(IconName.PLAY, QColor("white"))`
            Full control:

                ```python
                {
                    (QIcon.Mode.Normal, QIcon.State.Off): (IconName.PLAY, QColor("white")),
                    (QIcon.Mode.Normal, QIcon.State.On): (IconName.PAUSE, QColor("gray")),
                    (QIcon.Mode.Disabled, QIcon.State.Off): (IconName.PLAY, QColor("darkgray")),
                }
                ```
        size: Target size for SVG rendering (for crisp output).
    """
    icon = QIcon()
    render_size = size or QSize(256, 256)

    def _load_pixmap(name: IconName, color: QColor) -> QPixmap:
        return load_icon(name, render_size, color)

    # Simple icon
    if isinstance(icons, tuple):
        icon.addPixmap(_load_pixmap(*icons))
        return icon

    # Stateful icon
    for (mode, state), (name, color) in icons.items():
        icon.addPixmap(_load_pixmap(name, color), mode, state)

    return icon
make_tool_button
make_tool_button(
    icon: IconName | QIcon,
    tooltip: str,
    parent: QWidget | None = None,
    *,
    checkable: bool = False,
    checked: bool = False,
    register_icon: bool = True,
    icon_size: QSize | None = None,
    color: QColor | None = None,
    color_role: ColorRole = ToolTipText,
    icon_states: Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]]
    | None = None,
) -> QToolButton

Create a tool button with an icon and automatically register it for hot-reload when the icon is an IconName.

Parameters:

Name Type Description Default
icon IconName | QIcon

The icon to display (IconName for auto-creation, or QIcon for pre-made).

required
tooltip str

Tooltip text for the button.

required
parent QWidget | None

Parent widget.

None
checkable bool

Whether the button is checkable.

False
checked bool

Initial checked state (only applies if checkable=True).

False
icon_size QSize | None

Size for the icon.

None
color QColor | None

Explicit color for the icon (overrides color_role).

None
color_role ColorRole

Palette color role for the icon (default: ToolTipText). Used when icon_states is None.

ToolTipText
icon_states Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]] | None

Full mapping of (QIcon.Mode, QIcon.State) -> QPalette.ColorRole. Allows complete control over icon appearance for all Qt states:

  • Modes: Normal, Disabled, Active, Selected
  • States: Off, On

Example:

{
    (QIcon.Mode.Normal, QIcon.State.Off): QPalette.ColorRole.ToolTipText,
    (QIcon.Mode.Normal, QIcon.State.On): QPalette.ColorRole.Mid,
    (QIcon.Mode.Disabled, QIcon.State.Off): QPalette.ColorRole.Dark,
}

None

Returns:

Type Description
QToolButton

A configured QToolButton instance.

Source code in src/vsview/assets/utils.py
def make_tool_button(
    self,
    icon: IconName | QIcon,
    tooltip: str,
    parent: QWidget | None = None,
    *,
    checkable: bool = False,
    checked: bool = False,
    register_icon: bool = True,
    icon_size: QSize | None = None,
    color: QColor | None = None,
    color_role: QPalette.ColorRole = QPalette.ColorRole.ToolTipText,
    icon_states: Mapping[
        tuple[QIcon.Mode, QIcon.State],
        QPalette.ColorRole | tuple[QPalette.ColorGroup, QPalette.ColorRole],
    ]
    | None = None,
) -> QToolButton:
    """
    Create a tool button with an icon and automatically register it for hot-reload when the icon is an IconName.

    Args:
        icon: The icon to display (IconName for auto-creation, or QIcon for pre-made).
        tooltip: Tooltip text for the button.
        parent: Parent widget.
        checkable: Whether the button is checkable.
        checked: Initial checked state (only applies if checkable=True).
        icon_size: Size for the icon.
        color: Explicit color for the icon (overrides color_role).
        color_role: Palette color role for the icon (default: ToolTipText).
            Used when icon_states is None.
        icon_states: Full mapping of (QIcon.Mode, QIcon.State) -> QPalette.ColorRole.
            Allows complete control over icon appearance for all Qt states:

               - Modes: Normal, Disabled, Active, Selected
               - States: Off, On

            Example:
                ```python
                {
                    (QIcon.Mode.Normal, QIcon.State.Off): QPalette.ColorRole.ToolTipText,
                    (QIcon.Mode.Normal, QIcon.State.On): QPalette.ColorRole.Mid,
                    (QIcon.Mode.Disabled, QIcon.State.Off): QPalette.ColorRole.Dark,
                }
                ```

    Returns:
        A configured QToolButton instance.
    """
    btn = QToolButton(parent)
    btn.setCheckable(checkable)
    btn.setToolTip(tooltip)

    icon_size = icon_size or QSize(20, 20)

    if checkable:
        btn.setChecked(checked)

    if isinstance(icon, QIcon):
        btn.setIcon(icon)
    elif isinstance(icon, IconName):
        palette = btn.palette()
        q_icon = self._make_icon_from_iconname(icon, palette, icon_size, color, color_role, icon_states)
        btn.setIcon(q_icon)

        if register_icon:
            self.register_icon_button(btn, icon, icon_size, color_role, icon_states)

    btn.setIconSize(icon_size)
    btn.setAutoRaise(True)
    return btn
make_action
make_action(
    icon: IconName | QIcon,
    tooltip: str,
    parent: QWidget | None = None,
    *,
    checkable: bool = False,
    checked: bool = False,
    register_icon: bool = True,
    icon_size: QSize | None = None,
    color: QColor | None = None,
    color_role: ColorRole = ToolTipText,
    icon_states: Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]]
    | None = None,
) -> QAction

Create a QAction with an icon and automatically register it for hot-reload when the icon is an IconName.

Source code in src/vsview/assets/utils.py
def make_action(
    self,
    icon: IconName | QIcon,
    tooltip: str,
    parent: QWidget | None = None,
    *,
    checkable: bool = False,
    checked: bool = False,
    register_icon: bool = True,
    icon_size: QSize | None = None,
    color: QColor | None = None,
    color_role: QPalette.ColorRole = QPalette.ColorRole.ToolTipText,
    icon_states: Mapping[
        tuple[QIcon.Mode, QIcon.State],
        QPalette.ColorRole | tuple[QPalette.ColorGroup, QPalette.ColorRole],
    ]
    | None = None,
) -> QAction:
    """
    Create a QAction with an icon and automatically register it for hot-reload when the icon is an IconName.
    """
    act = QAction(parent or self, toolTip=tooltip, checkable=checkable, checked=checked)  # type: ignore[arg-type]

    icon_size = icon_size or QSize(20, 20)

    if isinstance(icon, QIcon):
        act.setIcon(icon)
    elif isinstance(icon, IconName):
        # Use self.palette() because QAction has no palette of its own
        palette = p.palette() if isinstance((p := act.parent()), QWidget) else getattr(self, "palette")()
        q_icon = self._make_icon_from_iconname(icon, palette, icon_size, color, color_role, icon_states)
        act.setIcon(q_icon)

        if register_icon:
            self.register_icon_action(act, icon, icon_size, color_role, icon_states)

    return act

LoginCredentialsInput

LoginCredentialsInput(parent: QWidget | None = None)

Bases: QWidget

Widget for entering login credentials.

Source code in src/vsview/app/settings/models.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)

    self.setLayout(layout := QVBoxLayout(self))
    layout.setContentsMargins(0, 0, 0, 0)
    layout.setSpacing(4)

    self.username_edit = QLineEdit(self, placeholderText="Username")
    layout.addWidget(self.username_edit)

    self.password_edit = QLineEdit(self, placeholderText="Password")
    self.password_edit.setEchoMode(QLineEdit.EchoMode.PasswordEchoOnEdit)
    layout.addWidget(self.password_edit)

ColorPickerInput

ColorPickerInput(parent: QWidget | None = None)

Bases: QWidget

Widget for selecting a color with a preview swatch and a hex entry.

Source code in src/vsview/app/settings/models.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)

    self.setLayout(layout := QHBoxLayout(self))
    layout.setContentsMargins(0, 0, 0, 0)
    layout.setSpacing(4)

    self.swatch = self.Swatch(self)
    self.swatch.setFixedSize(20, 20)
    self.swatch.clicked.connect(self._pick_color)
    layout.addWidget(self.swatch)

    self.hex_edit = QLineEdit(self, maxLength=9, placeholderText="#RRGGBBAA", alignment=Qt.AlignmentFlag.AlignRight)
    self.hex_edit.setFont(get_monospace_font())
    self.hex_edit.setFixedWidth(self.hex_edit.fontMetrics().horizontalAdvance("#FFFFFFFF") + 5)
    self.hex_edit.editingFinished.connect(self._on_hex_edited)

    layout.addWidget(self.hex_edit)

    layout.addStretch()

    self._color = QColor(Qt.GlobalColor.white)
    self._update_ui()
Classes
Swatch
Swatch(parent: QWidget | None = None)

Bases: QLabel

Swatch widget for displaying a color.

Source code in src/vsview/app/settings/models.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)

    self.setCursor(Qt.CursorShape.PointingHandCursor)

UI Settings

Classes

WidgetMetadata

WidgetMetadata(
    label: str,
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], Any] | None = None,
    from_ui: Callable[[Any], Any] | None = None,
)

Bases: ABC

Base class for widget metadata.

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

to_ui
to_ui: Callable[[Any], Any] | None = None

Transform value before loading into UI (e.g., seconds -> ms).

from_ui
from_ui: Callable[[Any], Any] | None = None

Transform value after extracting from UI (e.g., ms -> seconds).

Functions
create_widget
create_widget(parent: QWidget | None = None) -> W

Create and configure a widget for this metadata.

Source code in src/vsview/app/settings/models.py
@abstractmethod
def create_widget(self, parent: QWidget | None = None) -> W:
    """Create and configure a widget for this metadata."""
load_value
load_value(widget: W, value: Any) -> None

Load a value into the widget.

Source code in src/vsview/app/settings/models.py
@abstractmethod
def load_value(self, widget: W, value: Any) -> None:
    """Load a value into the widget."""
get_value
get_value(widget: W) -> Any

Get the current value from the widget.

Source code in src/vsview/app/settings/models.py
@abstractmethod
def get_value(self, widget: W) -> Any:
    """Get the current value from the widget."""

Checkbox

Checkbox(
    label: str,
    text: str,
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], Any] | None = None,
    from_ui: Callable[[Any], Any] | None = None,
    tristate: bool = False,
)

Bases: WidgetMetadata[QCheckBox]

Checkbox widget metadata.

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

to_ui
to_ui: Callable[[Any], Any] | None = None

Transform value before loading into UI (e.g., seconds -> ms).

from_ui
from_ui: Callable[[Any], Any] | None = None

Transform value after extracting from UI (e.g., ms -> seconds).

text
text: str

Text displayed next to the checkbox.

tristate
tristate: bool = field(default=False, kw_only=True)

Whether the checkbox is a tri-state checkbox

Dropdown

Dropdown(
    label: str,
    items: Iterable[tuple[str, Any]],
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], Any] | None = None,
    from_ui: Callable[[Any], Any] | None = None,
)

Bases: WidgetMetadata[QComboBox]

Dropdown/ComboBox widget metadata.

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

to_ui
to_ui: Callable[[Any], Any] | None = None

Transform value before loading into UI (e.g., seconds -> ms).

from_ui
from_ui: Callable[[Any], Any] | None = None

Transform value after extracting from UI (e.g., ms -> seconds).

items
items: Iterable[tuple[str, Any]]

Iterable of (display_text, value) tuples.

Spin

Spin(
    label: str,
    min: int = 0,
    max: int = 100,
    suffix: str = "",
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], Any] | None = None,
    from_ui: Callable[[Any], Any] | None = None,
)

Bases: WidgetMetadata[QSpinBox]

SpinBox widget metadata for integers.

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

to_ui
to_ui: Callable[[Any], Any] | None = None

Transform value before loading into UI (e.g., seconds -> ms).

from_ui
from_ui: Callable[[Any], Any] | None = None

Transform value after extracting from UI (e.g., ms -> seconds).

DoubleSpin

DoubleSpin(
    label: str,
    min: float = 0.0,
    max: float = 100.0,
    suffix: str = "",
    decimals: int = 2,
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], Any] | None = None,
    from_ui: Callable[[Any], Any] | None = None,
)

Bases: WidgetMetadata[QDoubleSpinBox]

DoubleSpinBox widget metadata for floats.

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

to_ui
to_ui: Callable[[Any], Any] | None = None

Transform value before loading into UI (e.g., seconds -> ms).

from_ui
from_ui: Callable[[Any], Any] | None = None

Transform value after extracting from UI (e.g., ms -> seconds).

PlainTextEdit

PlainTextEdit(
    label: str,
    value_type: type[T],
    max_height: int = 120,
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], Any] | None = None,
    from_ui: Callable[[Any], Any] | None = None,
    default_value: T | None = None,
)

Bases: WidgetMetadata[QPlainTextEdit]

PlainTextEdit widget for editing a list of values (one per line).

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

to_ui
to_ui: Callable[[Any], Any] | None = None

Transform value before loading into UI (e.g., seconds -> ms).

from_ui
from_ui: Callable[[Any], Any] | None = None

Transform value after extracting from UI (e.g., ms -> seconds).

value_type
value_type: type[T]

Type of values in the list.

max_height
max_height: int = 120

Maximum height of the widget in pixels.

default_value
default_value: T | None = field(default=None, kw_only=True)

Default value for the setting.

ListEdit

ListEdit(
    label: str,
    value_type: type[T],
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], Any] | None = None,
    from_ui: Callable[[Any], Any] | None = None,
    default_value: T | Sequence[T] | None = None,
    dialog_label_text: str | None = None,
    completions: Sequence[str] | None = None,
)

Bases: WidgetMetadata[ListEditWidget[T]]

ListEdit widget metadata using a QListWidget.

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

to_ui
to_ui: Callable[[Any], Any] | None = None

Transform value before loading into UI (e.g., seconds -> ms).

from_ui
from_ui: Callable[[Any], Any] | None = None

Transform value after extracting from UI (e.g., ms -> seconds).

value_type
value_type: type[T]

Type of values in the list.

default_value
default_value: T | Sequence[T] | None = field(default=None, kw_only=True)

Default value for the setting.

dialog_label_text
dialog_label_text: str | None = None

Label text for the dialog.

completions
completions: Sequence[str] | None = None

Completions for the dialog.

WidgetTimeEdit

WidgetTimeEdit(
    label: str,
    min: QTime | None = None,
    max: QTime | None = None,
    display_format: str | None = None,
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], QTime] | None = None,
    from_ui: Callable[[QTime], Any] | None = None,
)

Bases: WidgetMetadata[QTimeEdit]

TimeEdit widget for times

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

Login

Login(
    label: str,
    namespace: str,
    context: str,
    *,
    tooltip: str | None = None,
    to_ui: None = None,
    from_ui: None = None,
)

Bases: WidgetMetadata[LoginCredentialsInput]

Login credentials widget metadata.

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

namespace
namespace: str

Namespace for the secret.

context
context: str

Context for the secret

ColorPicker

ColorPicker(
    label: str,
    *,
    tooltip: str | None = None,
    to_ui: Callable[[Any], Any] | None = None,
    from_ui: Callable[[Any], Any] | None = None,
)

Bases: WidgetMetadata[ColorPickerInput]

ColorPicker widget metadata.

Attributes
label
label: str

Display label for the setting.

tooltip
tooltip: str | None = None

Tooltip text for the setting.

to_ui
to_ui: Callable[[Any], Any] | None = None

Transform value before loading into UI (e.g., seconds -> ms).

from_ui
from_ui: Callable[[Any], Any] | None = None

Transform value after extracting from UI (e.g., ms -> seconds).

Timeline & Playback

Classes

Frame

Bases: int

Frame number type.

Time

Bases: timedelta

Time type.

Functions
to_qtime
to_qtime() -> QTime

Convert a Time object to a QTime object.

Source code in src/vsview/app/views/timeline.py
def to_qtime(self) -> QTime:
    """Convert a Time object to a QTime object."""
    # QTime expects milliseconds since the start of the day
    total_ms = cround(self.total_seconds() * 1000)

    # Caps at 23:59:59.999. If delta > 24h, it wraps around.
    return QTime.fromMSecsSinceStartOfDay(total_ms)
to_ts
to_ts(fmt: str = '{H:02d}:{M:02d}:{S:02d}.{ms:03d}') -> str

Formats a timedelta object using standard Python formatting syntax.

Available keys: {D} : Days {H} : Hours (0-23) {M} : Minutes (0-59) {S} : Seconds (0-59) {ms} : Milliseconds (0-999) {us} : Microseconds (0-999999)

Total duration keys: {th} : Total Hours (e.g., 100 hours) {tm} : Total Minutes {ts} : Total Seconds

Example
# 1. Standard Clock format (Padding with :02d)
# Output: "26:05:03"
print(time.to_ts(td, "{th:02d}:{M:02d}:{S:02d}"))

# 2. Detailed format
# Output: "1 days, 02 hours, 05 minutes"
print(time.to_ts(td, "{D} days, {H:02d} hours, {M:02d} minutes"))

# 3. With Milliseconds
# Output: "02:05:03.500"
print(time.to_ts(td, "{H:02d}:{M:02d}:{S:02d}.{ms:03d}"))
Source code in src/vsview/app/views/timeline.py
def to_ts(self, fmt: str = "{H:02d}:{M:02d}:{S:02d}.{ms:03d}") -> str:
    """
    Formats a timedelta object using standard Python formatting syntax.

    Available keys:
    {D}  : Days
    {H}  : Hours (0-23)
    {M}  : Minutes (0-59)
    {S}  : Seconds (0-59)
    {ms} : Milliseconds (0-999)
    {us} : Microseconds (0-999999)

    Total duration keys:
    {th} : Total Hours (e.g., 100 hours)
    {tm} : Total Minutes
    {ts} : Total Seconds

    Example:
        ```python
        # 1. Standard Clock format (Padding with :02d)
        # Output: "26:05:03"
        print(time.to_ts(td, "{th:02d}:{M:02d}:{S:02d}"))

        # 2. Detailed format
        # Output: "1 days, 02 hours, 05 minutes"
        print(time.to_ts(td, "{D} days, {H:02d} hours, {M:02d} minutes"))

        # 3. With Milliseconds
        # Output: "02:05:03.500"
        print(time.to_ts(td, "{H:02d}:{M:02d}:{S:02d}.{ms:03d}"))
        ```

    """
    total_seconds = int(self.total_seconds())

    days = self.days
    hours, remainder = divmod(self.seconds, 3600)
    minutes, seconds = divmod(remainder, 60)

    milliseconds = cround(self.microseconds / 1000)

    format_data = {
        "D": days,
        "H": hours,
        "M": minutes,
        "S": seconds,
        "ms": milliseconds,
        "us": self.microseconds,
        # Total durations (useful for "26 hours ago")
        "th": total_seconds // 3600,
        "tm": total_seconds // 60,
        "ts": total_seconds,
    }

    return fmt.format(**format_data)
from_qtime
from_qtime(qtime: QTime) -> Self

Convert a QTime object to a Time object.

Source code in src/vsview/app/views/timeline.py
@classmethod
def from_qtime(cls, qtime: QTime) -> Self:
    """Convert a QTime object to a Time object."""
    return cls(milliseconds=qtime.msecsSinceStartOfDay())

Utilities

Attributes

hookimpl

hookimpl = HookimplMarker('vsview')

Marker to be used for hook implementations.

Classes

Packer

Packer(bit_depth: int)

Bases: ABC

Abstract base class for RGB packers.

Source code in src/vsview/app/outputs/packing.py
def __init__(self, bit_depth: int) -> None:
    self.bit_depth = bit_depth
    self.vs_format, self.vs_aformat, self.qt_format, self.qt_aformat = Packer.FORMAT_CONFIG[bit_depth]
Functions
to_rgb_planar
to_rgb_planar(clip: VideoNode, **kwargs: Any) -> VideoNode

Converts clip to planar vs.RGB24 or vs.RGB30.

Source code in src/vsview/app/outputs/packing.py
def to_rgb_planar(self, clip: vs.VideoNode, **kwargs: Any) -> vs.VideoNode:
    """Converts clip to planar vs.RGB24 or vs.RGB30."""
    if (ppolicy := SettingsManager.global_settings.view.props_policy) != "error":
        warned: dict[str, dict[IntEnum, bool]] = {"_Transfer": {}, "_Primaries": {}}
        clip = clip.std.ModifyFrame(clip, lambda n, f: _normalize_color_props(n, f, ppolicy, warned))

    params = dict[str, Any](
        format=self.vs_format,
        dither_type=SettingsManager.global_settings.view.dither_type,
        resample_filter_uv=SettingsManager.global_settings.view.chroma_resizer.vs_func,
        filter_param_a_uv=SettingsManager.global_settings.view.chroma_resizer.param_a,
        filter_param_b_uv=SettingsManager.global_settings.view.chroma_resizer.param_b,
        transfer=vs.TRANSFER_BT709,
        primaries=vs.PRIMARIES_BT709,
    )
    return clip.resize.Point(**params | kwargs)
to_rgb_packed
to_rgb_packed(
    clip: VideoNode, alpha: VideoNode | Literal[True] | None = None
) -> VideoNode

Converts planar vs.RGB24 or vs.RGB30 to interleaved BGRA32 or RGB30 to packed A2R10G10B10

Source code in src/vsview/app/outputs/packing.py
@abstractmethod
def to_rgb_packed(self, clip: vs.VideoNode, alpha: vs.VideoNode | Literal[True] | None = None) -> vs.VideoNode:
    """Converts planar vs.RGB24 or vs.RGB30 to interleaved BGRA32 or RGB30 to packed A2R10G10B10"""
pack_clip
pack_clip(clip: VideoNode, alpha: VideoNode | Literal[True] | None = None) -> VideoNode

Converts a planar VideoNode and an optional alpha mask to a packed RGB/RGBA VideoNode.

Source code in src/vsview/app/outputs/packing.py
def pack_clip(self, clip: vs.VideoNode, alpha: vs.VideoNode | Literal[True] | None = None) -> vs.VideoNode:
    """Converts a planar VideoNode and an optional alpha mask to a packed RGB/RGBA VideoNode."""
    if isinstance(alpha, vs.VideoNode):
        alpha = alpha.resize.Point(
            format=self.vs_aformat,
            dither_type=SettingsManager.global_settings.view.dither_type,
        )

    planar = self.to_rgb_planar(clip)
    packed = self.to_rgb_packed(planar, alpha)

    return packed.std.SetFrameProp("VSViewHasAlpha", True) if alpha else packed
frame_to_qimage
frame_to_qimage(frame: VideoFrame, **kwargs: Any) -> QImage

Wraps a packed VapourSynth VideoFrame into a QImage.

If the copy_qimage setting is enabled, ownership of the memory is transferred to Qt by returning a copy of the image. Otherwise, the returned QImage does not own its memory and points directly to the VapourSynth frame's buffer.

Warning

When copy_qimage is disabled, you MUST either keep the source frame alive as long as the QImage is used, or call .copy() on the returned QImage.

Source code in src/vsview/app/outputs/packing.py
def frame_to_qimage(self, frame: vs.VideoFrame, **kwargs: Any) -> QImage:
    """
    Wraps a packed VapourSynth VideoFrame into a QImage.

    If the `copy_qimage` setting is enabled, ownership of the memory is transferred to Qt
    by returning a copy of the image.
    Otherwise, the returned QImage **does not own its memory** and points directly
    to the VapourSynth frame's buffer.

    !!! warning
        When `copy_qimage` is disabled, you MUST either keep the source `frame` alive as long
        as the QImage is used, or call ``.copy()`` on the returned QImage.
    """

    alpha = "VSViewHasAlpha" in frame.props or "_Alpha" in frame.props

    params = dict[str, Any](format=self.qt_aformat if alpha else self.qt_format) | kwargs

    # QImage supports Buffer inputs
    img = QImage(
        get_plane_buffer(frame, 0),  # type: ignore[call-overload]
        frame.width,
        frame.height,
        frame.get_stride(0),
        params.pop("format"),
        **params,
    )

    if SettingsManager.global_settings.view.copy_qimage:
        return img.copy()

    return img

IconName

Bases: StrEnum

IconReloadMixin

IconReloadMixin(*args: Any, **kwargs: Any)

Mixin for QWidget subclasses with automatic icon hot-reload support.

Mix this with any QWidget subclass to get automatic icon updates when global settings (icon provider/weight) change.

Example:

class MyWidget(QWidget, IconReloadMixin):
    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)

        # Button is automatically registered for icon hot-reload
        self.btn = self.make_tool_button(IconName.LINK, "Link", self)

Source code in src/vsview/assets/utils.py
def __init__(self, *args: Any, **kwargs: Any) -> None:
    super().__init__(*args, **kwargs)
    self._button_reloaders = WeakKeyDictionary[QToolButton, Callable[[], None]]()
    self._action_reloaders = WeakKeyDictionary[QAction, Callable[[], None]]()
    self._custom_callbacks = list[Callable[[], None]]()

    from ..app.settings import SettingsManager

    SettingsManager.signals.globalChanged.connect(lambda: QTimer.singleShot(0, self._reload_all_icons))
Functions
register_icon_button
register_icon_button(
    button: QToolButton,
    icon_name: IconName,
    icon_size: QSize | None = None,
    color_role: ColorRole = ToolTipText,
    icon_states: Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]]
    | None = None,
) -> None

Register a button for automatic icon reload when settings change.

Parameters:

Name Type Description Default
button QToolButton

The QToolButton to update.

required
icon_name IconName

The IconName to use for the button.

required
icon_size QSize | None

Size for the icon.

None
color_role ColorRole

Palette color role for simple icons (used when icon_states is None).

ToolTipText
icon_states Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]] | None

Full mapping of (QIcon.Mode, QIcon.State) -> QPalette.ColorRole. Allows complete control over icon appearance for all Qt states.

Example:

{
    (QIcon.Mode.Normal, QIcon.State.Off): QPalette.ColorRole.ToolTipText,
    (QIcon.Mode.Normal, QIcon.State.On): QPalette.ColorRole.Highlight,
    (QIcon.Mode.Disabled, QIcon.State.Off): QPalette.ColorRole.Mid,
}

None
Source code in src/vsview/assets/utils.py
def register_icon_button(
    self,
    button: QToolButton,
    icon_name: IconName,
    icon_size: QSize | None = None,
    color_role: QPalette.ColorRole = QPalette.ColorRole.ToolTipText,
    icon_states: Mapping[
        tuple[QIcon.Mode, QIcon.State],
        QPalette.ColorRole | tuple[QPalette.ColorGroup, QPalette.ColorRole],
    ]
    | None = None,
) -> None:
    """
    Register a button for automatic icon reload when settings change.

    Args:
        button: The QToolButton to update.
        icon_name: The IconName to use for the button.
        icon_size: Size for the icon.
        color_role: Palette color role for simple icons (used when icon_states is None).
        icon_states: Full mapping of (QIcon.Mode, QIcon.State) -> QPalette.ColorRole.
            Allows complete control over icon appearance for all Qt states.

            Example:
                ```python
                {
                    (QIcon.Mode.Normal, QIcon.State.Off): QPalette.ColorRole.ToolTipText,
                    (QIcon.Mode.Normal, QIcon.State.On): QPalette.ColorRole.Highlight,
                    (QIcon.Mode.Disabled, QIcon.State.Off): QPalette.ColorRole.Mid,
                }
                ```
    """

    def reload(btn: QToolButton) -> None:
        palette = btn.palette()

        if icon_states:
            icon = self.make_icon(
                {
                    (mode, state): (
                        icon_name,
                        palette.color(*role) if isinstance(role, tuple) else palette.color(role),
                    )
                    for (mode, state), role in icon_states.items()
                },
                size=icon_size,
            )
        else:
            color = palette.color(QPalette.ColorGroup.Normal, color_role)
            icon = self.make_icon((icon_name, color), size=icon_size)
        btn.setIcon(icon)

    self._button_reloaders[button] = partial(reload, button)
register_icon_action
register_icon_action(
    action: QAction,
    icon_name: IconName,
    icon_size: QSize | None = None,
    color_role: ColorRole = ToolTipText,
    icon_states: Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]]
    | None = None,
) -> None

Register a QAction for automatic icon reload.

Source code in src/vsview/assets/utils.py
def register_icon_action(
    self,
    action: QAction,
    icon_name: IconName,
    icon_size: QSize | None = None,
    color_role: QPalette.ColorRole = QPalette.ColorRole.ToolTipText,
    icon_states: Mapping[
        tuple[QIcon.Mode, QIcon.State],
        QPalette.ColorRole | tuple[QPalette.ColorGroup, QPalette.ColorRole],
    ]
    | None = None,
) -> None:
    """Register a QAction for automatic icon reload."""

    def reload(act: QAction) -> None:
        # Action doesn't have a palette, so we use the parent widget's palette (self)
        palette = p.palette() if isinstance((p := action.parent()), QWidget) else getattr(self, "palette")()

        if icon_states:
            icon = self.make_icon(
                {
                    (mode, state): (
                        icon_name,
                        palette.color(*role) if isinstance(role, tuple) else palette.color(role),
                    )
                    for (mode, state), role in icon_states.items()
                },
                size=icon_size,
            )
        else:
            color = palette.color(QPalette.ColorGroup.Normal, color_role)
            icon = self.make_icon((icon_name, color), size=icon_size)
        act.setIcon(icon)

    self._action_reloaders[action] = partial(reload, action)
register_icon_callback
register_icon_callback(callback: Callable[[], None]) -> None

Register a custom callback for icon reloading.

Use this for complex icons that don't fit the standard button pattern (e.g., play/pause with different icons per state, spinner animations).

Parameters:

Name Type Description Default
callback Callable[[], None]

A callable that reloads the icon(s).

required
Source code in src/vsview/assets/utils.py
def register_icon_callback(self, callback: Callable[[], None]) -> None:
    """
    Register a custom callback for icon reloading.

    Use this for complex icons that don't fit the standard button pattern
    (e.g., play/pause with different icons per state, spinner animations).

    Args:
        callback: A callable that reloads the icon(s).
    """
    self._custom_callbacks.append(callback)
make_icon
make_icon(
    icons: tuple[IconName, QColor] | dict[tuple[Mode, State], tuple[IconName, QColor]],
    size: QSize | None = None,
) -> QIcon

Create a QIcon from either: - a single (IconName, color) tuple - or a dict mapping (mode, state) -> (IconName, color)

Parameters:

Name Type Description Default
icons tuple[IconName, QColor] | dict[tuple[Mode, State], tuple[IconName, QColor]]

Icon specification using IconName enum. Simple usage: (IconName.PLAY, QColor("white")) Full control:

```python
{
    (QIcon.Mode.Normal, QIcon.State.Off): (IconName.PLAY, QColor("white")),
    (QIcon.Mode.Normal, QIcon.State.On): (IconName.PAUSE, QColor("gray")),
    (QIcon.Mode.Disabled, QIcon.State.Off): (IconName.PLAY, QColor("darkgray")),
}
```
required
size QSize | None

Target size for SVG rendering (for crisp output).

None
Source code in src/vsview/assets/utils.py
@staticmethod
def make_icon(
    icons: tuple[IconName, QColor] | dict[tuple[QIcon.Mode, QIcon.State], tuple[IconName, QColor]],
    size: QSize | None = None,
) -> QIcon:
    """
    Create a QIcon from either:
    - a single (IconName, color) tuple
    - or a dict mapping (mode, state) -> (IconName, color)

    Args:
        icons: Icon specification using IconName enum.
            Simple usage: `(IconName.PLAY, QColor("white"))`
            Full control:

                ```python
                {
                    (QIcon.Mode.Normal, QIcon.State.Off): (IconName.PLAY, QColor("white")),
                    (QIcon.Mode.Normal, QIcon.State.On): (IconName.PAUSE, QColor("gray")),
                    (QIcon.Mode.Disabled, QIcon.State.Off): (IconName.PLAY, QColor("darkgray")),
                }
                ```
        size: Target size for SVG rendering (for crisp output).
    """
    icon = QIcon()
    render_size = size or QSize(256, 256)

    def _load_pixmap(name: IconName, color: QColor) -> QPixmap:
        return load_icon(name, render_size, color)

    # Simple icon
    if isinstance(icons, tuple):
        icon.addPixmap(_load_pixmap(*icons))
        return icon

    # Stateful icon
    for (mode, state), (name, color) in icons.items():
        icon.addPixmap(_load_pixmap(name, color), mode, state)

    return icon
make_tool_button
make_tool_button(
    icon: IconName | QIcon,
    tooltip: str,
    parent: QWidget | None = None,
    *,
    checkable: bool = False,
    checked: bool = False,
    register_icon: bool = True,
    icon_size: QSize | None = None,
    color: QColor | None = None,
    color_role: ColorRole = ToolTipText,
    icon_states: Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]]
    | None = None,
) -> QToolButton

Create a tool button with an icon and automatically register it for hot-reload when the icon is an IconName.

Parameters:

Name Type Description Default
icon IconName | QIcon

The icon to display (IconName for auto-creation, or QIcon for pre-made).

required
tooltip str

Tooltip text for the button.

required
parent QWidget | None

Parent widget.

None
checkable bool

Whether the button is checkable.

False
checked bool

Initial checked state (only applies if checkable=True).

False
icon_size QSize | None

Size for the icon.

None
color QColor | None

Explicit color for the icon (overrides color_role).

None
color_role ColorRole

Palette color role for the icon (default: ToolTipText). Used when icon_states is None.

ToolTipText
icon_states Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]] | None

Full mapping of (QIcon.Mode, QIcon.State) -> QPalette.ColorRole. Allows complete control over icon appearance for all Qt states:

  • Modes: Normal, Disabled, Active, Selected
  • States: Off, On

Example:

{
    (QIcon.Mode.Normal, QIcon.State.Off): QPalette.ColorRole.ToolTipText,
    (QIcon.Mode.Normal, QIcon.State.On): QPalette.ColorRole.Mid,
    (QIcon.Mode.Disabled, QIcon.State.Off): QPalette.ColorRole.Dark,
}

None

Returns:

Type Description
QToolButton

A configured QToolButton instance.

Source code in src/vsview/assets/utils.py
def make_tool_button(
    self,
    icon: IconName | QIcon,
    tooltip: str,
    parent: QWidget | None = None,
    *,
    checkable: bool = False,
    checked: bool = False,
    register_icon: bool = True,
    icon_size: QSize | None = None,
    color: QColor | None = None,
    color_role: QPalette.ColorRole = QPalette.ColorRole.ToolTipText,
    icon_states: Mapping[
        tuple[QIcon.Mode, QIcon.State],
        QPalette.ColorRole | tuple[QPalette.ColorGroup, QPalette.ColorRole],
    ]
    | None = None,
) -> QToolButton:
    """
    Create a tool button with an icon and automatically register it for hot-reload when the icon is an IconName.

    Args:
        icon: The icon to display (IconName for auto-creation, or QIcon for pre-made).
        tooltip: Tooltip text for the button.
        parent: Parent widget.
        checkable: Whether the button is checkable.
        checked: Initial checked state (only applies if checkable=True).
        icon_size: Size for the icon.
        color: Explicit color for the icon (overrides color_role).
        color_role: Palette color role for the icon (default: ToolTipText).
            Used when icon_states is None.
        icon_states: Full mapping of (QIcon.Mode, QIcon.State) -> QPalette.ColorRole.
            Allows complete control over icon appearance for all Qt states:

               - Modes: Normal, Disabled, Active, Selected
               - States: Off, On

            Example:
                ```python
                {
                    (QIcon.Mode.Normal, QIcon.State.Off): QPalette.ColorRole.ToolTipText,
                    (QIcon.Mode.Normal, QIcon.State.On): QPalette.ColorRole.Mid,
                    (QIcon.Mode.Disabled, QIcon.State.Off): QPalette.ColorRole.Dark,
                }
                ```

    Returns:
        A configured QToolButton instance.
    """
    btn = QToolButton(parent)
    btn.setCheckable(checkable)
    btn.setToolTip(tooltip)

    icon_size = icon_size or QSize(20, 20)

    if checkable:
        btn.setChecked(checked)

    if isinstance(icon, QIcon):
        btn.setIcon(icon)
    elif isinstance(icon, IconName):
        palette = btn.palette()
        q_icon = self._make_icon_from_iconname(icon, palette, icon_size, color, color_role, icon_states)
        btn.setIcon(q_icon)

        if register_icon:
            self.register_icon_button(btn, icon, icon_size, color_role, icon_states)

    btn.setIconSize(icon_size)
    btn.setAutoRaise(True)
    return btn
make_action
make_action(
    icon: IconName | QIcon,
    tooltip: str,
    parent: QWidget | None = None,
    *,
    checkable: bool = False,
    checked: bool = False,
    register_icon: bool = True,
    icon_size: QSize | None = None,
    color: QColor | None = None,
    color_role: ColorRole = ToolTipText,
    icon_states: Mapping[tuple[Mode, State], ColorRole | tuple[ColorGroup, ColorRole]]
    | None = None,
) -> QAction

Create a QAction with an icon and automatically register it for hot-reload when the icon is an IconName.

Source code in src/vsview/assets/utils.py
def make_action(
    self,
    icon: IconName | QIcon,
    tooltip: str,
    parent: QWidget | None = None,
    *,
    checkable: bool = False,
    checked: bool = False,
    register_icon: bool = True,
    icon_size: QSize | None = None,
    color: QColor | None = None,
    color_role: QPalette.ColorRole = QPalette.ColorRole.ToolTipText,
    icon_states: Mapping[
        tuple[QIcon.Mode, QIcon.State],
        QPalette.ColorRole | tuple[QPalette.ColorGroup, QPalette.ColorRole],
    ]
    | None = None,
) -> QAction:
    """
    Create a QAction with an icon and automatically register it for hot-reload when the icon is an IconName.
    """
    act = QAction(parent or self, toolTip=tooltip, checkable=checkable, checked=checked)  # type: ignore[arg-type]

    icon_size = icon_size or QSize(20, 20)

    if isinstance(icon, QIcon):
        act.setIcon(icon)
    elif isinstance(icon, IconName):
        # Use self.palette() because QAction has no palette of its own
        palette = p.palette() if isinstance((p := act.parent()), QWidget) else getattr(self, "palette")()
        q_icon = self._make_icon_from_iconname(icon, palette, icon_size, color, color_role, icon_states)
        act.setIcon(q_icon)

        if register_icon:
            self.register_icon_action(act, icon, icon_size, color_role, icon_states)

    return act

Functions

get_packer

get_packer(method: str | None = None, bit_depth: int | None = None) -> Packer

Get the packer to use for packing clips.

Parameters:

Name Type Description Default
method str | None

The packing method to use. If None, the global setting will be used.

None
bit_depth int | None

The bit depth to use. If None, the global setting will be used.

None

Returns:

Type Description
Packer

The packer to use for packing clips.

Source code in src/vsview/app/outputs/packing.py
def get_packer(method: str | None = None, bit_depth: int | None = None) -> Packer:
    """
    Get the packer to use for packing clips.

    Args:
        method: The packing method to use. If None, the global setting will be used.
        bit_depth: The bit depth to use. If None, the global setting will be used.

    Returns:
        The packer to use for packing clips.
    """
    method = method or SettingsManager.global_settings.view.packing_method
    bit_depth = bit_depth or SettingsManager.global_settings.view.bit_depth

    if method == "auto":
        method = "vszip" if _is_vszip_available() else "cython"
        logger.debug("Auto-selected packing method: %s", method)

    match method:
        case "vszip":
            if not _is_vszip_available():
                logger.warning("vszip plugin is not available, falling back to Cython (8-bit) packer")
                return CythonPacker(8)

            return VszipPacker(bit_depth)

        case "cython":
            return CythonPacker(bit_depth)

        case "numpy":
            return NumpyPacker(bit_depth)

        case "python":
            return PythonPacker(bit_depth)

        case _:
            raise NotImplementedError

run_in_background

run_in_background[**P, R](func: _CoroutineFunc[P, R]) -> Callable[P, Future[R]]
run_in_background[**P, R](func: _Func[P, R]) -> Callable[P, Future[R]]
run_in_background(*, name: str) -> _DecoratorFuture

Executes the decorated function in a background thread (via QThreadPool) using the QtEventLoop's to_thread logic.

Parameters:

Name Type Description Default
func Any

The function to wrap (when used as @run_in_background without parens)

None
name str | None

Optional thread name for logging (when used as @run_in_background(name="..."))

None

Returns:

Type Description
Any

A future object representing the result of the execution.

Usage:

@run_in_background
def my_func(): ...


@run_in_background(name="MyWorker")
def my_named_func(): ...

Source code in src/vsview/vsenv/loop.py
def run_in_background(func: Any = None, *, name: str | None = None) -> Any:
    """
    Executes the decorated function in a background thread (via QThreadPool)
    using the `QtEventLoop`'s `to_thread` logic.

    Args:
        func: The function to wrap (when used as `@run_in_background` without parens)
        name: Optional thread name for logging (when used as `@run_in_background(name="...")`)

    Returns:
        A future object representing the result of the execution.

    Usage:
    ```python
    @run_in_background
    def my_func(): ...


    @run_in_background(name="MyWorker")
    def my_named_func(): ...
    ```
    """

    def decorator(fn: Any) -> Any:
        @wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            loop = cast(QtEventLoop, get_loop())

            if iscoroutinefunction(fn):

                def run_coro() -> Any:
                    import asyncio

                    coro = fn(*args, **kwargs)
                    try:
                        return asyncio.run(coro)
                    except RuntimeError:
                        return asyncio.run_coroutine_threadsafe(coro, asyncio.get_running_loop()).result()

                return loop.to_thread(run_coro) if name is None else loop.to_thread_named(name, run_coro)

            return (
                loop.to_thread(fn, *args, **kwargs) if name is None else loop.to_thread_named(name, fn, *args, **kwargs)
            )

        return wrapper

    return decorator if func is None else decorator(func)

run_in_loop

run_in_loop[**P, R](func: _CoroutineFunc[P, R]) -> Callable[P, Future[R]]
run_in_loop[**P, R](func: _Func[P, R]) -> Callable[P, Future[R]]
run_in_loop(*, return_future: Literal[True]) -> _DecoratorFuture
run_in_loop(*, return_future: Literal[False]) -> _DecoratorDirect
run_in_loop(*, return_future: bool) -> _DecoratorFuture | _DecoratorDirect

Decorator. Executes the decorated function within the QtEventLoop (Main Thread).

Parameters:

Name Type Description Default
func Any

The function to wrap (when used as @run_in_loop without parens)

None
return_future bool

If False, blocks and returns R directly.

True

Returns:

Type Description
Any

A future object or the result directly, depending on return_future.

Usage:

@run_in_loop
def my_func(): ...


@run_in_loop(return_future=False)
def my_blocking_func(): ...

Source code in src/vsview/vsenv/loop.py
def run_in_loop(func: Any = None, *, return_future: bool = True) -> Any:
    """
    Decorator. Executes the decorated function within the `QtEventLoop` (Main Thread).

    Args:
        func: The function to wrap (when used as `@run_in_loop` without parens)
        return_future: If False, blocks and returns R directly.

    Returns:
        A future object or the result directly, depending on return_future.

    Usage:
    ```python
    @run_in_loop
    def my_func(): ...


    @run_in_loop(return_future=False)
    def my_blocking_func(): ...
    ```
    """

    def decorator(fn: Any) -> Any:
        @wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            loop = cast(QtEventLoop, get_loop())

            if iscoroutinefunction(fn):

                def run_coro() -> Any:
                    import asyncio

                    coro = fn(*args, **kwargs)
                    try:
                        return asyncio.run(coro)
                    except RuntimeError:
                        return asyncio.run_coroutine_threadsafe(coro, asyncio.get_running_loop()).result()

                fut = loop.from_thread(run_coro)
            else:
                # Delegate to from_thread to marshal execution to the main loop
                fut = loop.from_thread(fn, *args, **kwargs)

            return fut if return_future else fut.result()

        return wrapper

    return decorator if func is None else decorator(func)