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

    SettingsManager.signals.globalChanged.connect(self._on_global_settings_changed)
    SettingsManager.signals.localChanged.connect(self._on_local_settings_changed)
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.

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.

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.

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
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,
    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.

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)

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().

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.

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
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_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._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()

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)
Functions
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

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,
)

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.

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.

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.

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

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 = QSize(20, 20),
    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

Size for the icon.

QSize(20, 20)
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 = QSize(20, 20),
    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 = QSize(20, 20),
    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 = QSize(20, 20),
    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 = QSize(20, 20),
    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

Size for the icon.

QSize(20, 20)
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 = QSize(20, 20),
    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)

    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 = QSize(20, 20),
    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 = QSize(20, 20),
    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]

    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

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)

            else:
                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)