simvx.editor.workspace_tabs

Workspace Tabs — Unified tab bar for scene and script tabs.

Data model and manager for the VS Code-style unified tab bar where scene tabs and script tabs coexist. Multiple scenes can be open simultaneously, each with its own undo/selection/camera state.

Module Contents

Classes

SceneTabState

Per-scene snapshot holding all scene-specific state.

ScriptTabState

Per-script tab state.

UntitledTabState

Editor-only scratch buffer — VS Code “Untitled-N” pattern.

WorkspaceTabs

Unified tab manager for scene and script tabs.

UnsavedChangesDialog

Modal confirmation dialog shown when closing a tab with unsaved changes.

NewSceneDialog

Modal overlay for choosing a new scene root type.

Data

log

API

simvx.editor.workspace_tabs.log

‘getLogger(…)’

class simvx.editor.workspace_tabs.SceneTabState[source]

Per-scene snapshot holding all scene-specific state.

scene_tree: simvx.core.SceneTree

None

scene_path: pathlib.Path | None

None

selection: simvx.core.Selection

‘field(…)’

undo_stack: simvx.core.UndoStack

‘field(…)’

editor_camera: simvx.core.OrbitCamera3D

‘field(…)’

viewport_sub_mode: str

‘3d’

modified: bool

False

saved_scene_data: dict | None

None

playing_root: simvx.core.Node | None

None

placeholder: simvx.core.Control

‘field(…)’

tab_name: str

‘Untitled’

source_file: str | None

None

source_class: type | None

None

source_module: Any

None

file_classification: str = <Multiline-String>
identity_hints: dict

‘field(…)’

property tab_widget: simvx.core.Control[source]

The widget representing this tab in a TabContainer.

property is_dirty: bool[source]

Whether this tab has unsaved changes.

save() None[source]

Mark clean; actual file save is handled by State/SceneFileOps.

classmethod create(root_type: type = Node, name: str = 'Root') simvx.editor.workspace_tabs.SceneTabState[source]

Create a fresh scene tab with sensible defaults.

class simvx.editor.workspace_tabs.ScriptTabState[source]

Per-script tab state.

key: str

None

kind: str

None

node: simvx.core.Node | None

None

editor: simvx.core.CodeTextEdit

None

saved_text: str

None

tab_name: str

None

__post_init__()[source]
property tab_widget: simvx.core.Control[source]

The widget representing this tab in a TabContainer.

property is_dirty: bool[source]

Whether this tab has unsaved changes (O(1) hash check).

save() None[source]

Persist the current editor text back to its source.

File-backed tabs opened via open_file() (no owning node) write straight to the key path; other kinds route through the script Node.

class simvx.editor.workspace_tabs.UntitledTabState[source]

Editor-only scratch buffer — VS Code “Untitled-N” pattern.

Holds a Python source buffer that has not yet been saved to disk. The buffer lives purely in editor memory (this dataclass + a CodeTextEdit widget); it is never serialised onto a Node attribute or any scene file. On save (Ctrl+S) the editor prompts for a destination path; once written the tab is replaced with a regular file-backed ScriptTabState.

ordinal is the auto-assigned 1-based sequence number that produced the default tab name Untitled-N.py. The :class:WorkspaceTabs keeps track of the next ordinal so closing and reopening Untitled tabs reuses the lowest available number.

ordinal: int

None

editor: simvx.core.CodeTextEdit

None

tab_name: str

None

__post_init__()[source]
property tab_widget: simvx.core.Control[source]

The widget representing this tab in a TabContainer.

property is_dirty: bool[source]

Untitled buffers are dirty as soon as the user types anything.

An empty Untitled buffer is treated as clean — closing it without any input should not prompt to save.

save() None[source]

No-op — actual save routes through the editor’s Save-As file dialog.

Implemented for tab-protocol parity (e.g. WorkspaceTabs.save_all_scripts iterating all tabs). The promotion to a ScriptTabState happens in

Meth:

WorkspaceTabs.promote_untitled_to_file after the user picks a destination path.

class simvx.editor.workspace_tabs.WorkspaceTabs[source]

Unified tab manager for scene and script tabs.

Initialization

MODIFIED_TEXT_COLOUR

(0.95, 0.78, 0.35, 1.0)

MODIFIED_PREFIX

‘● ‘

bind(tab_container: simvx.core.TabContainer)[source]

Bind to a TabContainer widget for display.

property tab_count: int[source]
property active_index: int[source]
is_scene_tab(i: int) bool[source]
is_script_tab(i: int) bool[source]
add_scene_tab(state: simvx.editor.workspace_tabs.SceneTabState) int[source]

Add a scene tab and return its index.

property active_scene: simvx.editor.workspace_tabs.SceneTabState | None[source]

The active scene tab state, or None if a script tab is active.

find_scene_tab(path: pathlib.Path) int | None[source]

Find a scene tab by file path.

open_script(node: simvx.core.Node, project_path_fn=None, line: int | None = None) int[source]

Open or switch to a script tab for the given node.

When line is given (1-indexed), position the cursor at that line after opening/switching. Used for error navigation.

open_file(path, line: int | None = None) int[source]

Open or switch to a script tab backed by a file path.

Useful for error navigation from the console: the traceback identifies a file path on disk, which may or may not be attached to a node. If a tab already exists for this path, switch to it; otherwise open a new file-backed script tab.

save_all_scripts()[source]

Save all dirty script tabs.

save_current_script()[source]

Save the active script tab, if any.

property current_editor: simvx.core.CodeTextEdit | None[source]

The active script or Untitled tab’s editor, if any.

is_untitled_tab(i: int) bool[source]
untitled_tabs() list[simvx.editor.workspace_tabs.UntitledTabState][source]

All currently-open Untitled scratch buffers (in tab order).

new_untitled() int[source]

Open a fresh empty Untitled scratch buffer; return its tab index.

The buffer lives purely in editor state. Promotion to a real file-backed tab happens via :meth:promote_untitled_to_file after the user invokes Save and picks a path through the file dialog.

promote_untitled_to_file(index: int, path: pathlib.Path | str) bool[source]

Convert an Untitled tab at index into a file-backed script tab.

Writes the current editor text to path, then replaces the

Class:

UntitledTabState slot with a fresh :class:ScriptTabState keyed off the resolved file path. The same CodeTextEdit widget is reused so the tab container does not need to be rebuilt.

Returns True on success, False if the path could not be written (the Untitled tab is left untouched in that case).

set_active(index: int)[source]

Switch to tab at index.

close_tab(index: int)[source]

Close a tab by index.

close_current_tab()[source]

Close the currently active tab.

close_all_tabs(on_complete: collections.abc.Callable | None = None)[source]

Close all tabs sequentially, prompting for unsaved changes. Calls on_complete when done.

is_tab_dirty(index: int) bool[source]

Return whether the tab at index has unsaved changes.

class simvx.editor.workspace_tabs.UnsavedChangesDialog(**kwargs)[source]

Bases: simvx.core.Panel

Modal confirmation dialog shown when closing a tab with unsaved changes.

Initialization

DIALOG_WIDTH

300.0

DIALOG_HEIGHT

160.0

BUTTON_HEIGHT

32.0

BUTTON_GAP

8.0

show_dialog(tab_name: str = '', parent_size: simvx.core.Vec2 | None = None)[source]

Show the dialog with the given tab name in the message.

gui_input(event)[source]

Handle escape to cancel.

style

‘ThemeStyleBox(…)’

property bg_colour
property border_colour
property border_width
get_minimum_size() simvx.core.math.types.Vec2
draw(renderer)
size_x

‘Property(…)’

size_y

‘Property(…)’

anchor_left

‘Property(…)’

anchor_top

‘Property(…)’

anchor_right

‘Property(…)’

anchor_bottom

‘Property(…)’

margin_left

‘Property(…)’

margin_top

‘Property(…)’

margin_right

‘Property(…)’

margin_bottom

‘Property(…)’

property size: simvx.core.math.types.Vec2
touch_mode: str

‘mouse’

property theme: simvx.core.ui.types.Theme | None
property mouse_over: bool
property focused: bool
property disabled: bool
get_theme() simvx.core.ui.types.Theme
queue_redraw()
get_rect() tuple[float, float, float, float]
get_global_rect() tuple[float, float, float, float]
is_point_inside(point) bool
set_anchor_preset(preset: simvx.core.ui.enums.AnchorPreset)
set_focus()
grab_focus()
release_focus()
has_focus() bool
focus_next_control()
focus_previous_control()
grab_mouse()
release_mouse()
set_drag_preview(control: simvx.core.ui.core.Control)
draw_popup(renderer)
is_popup_point_inside(point) bool
popup_input(event)
dismiss_popup()
z_index

‘Property(…)’

z_as_relative

‘Property(…)’

render_layer

‘Property(…)’

set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
property absolute_z_index: int
property position: simvx.core.math.types.Vec2
property rotation: float
property rotation_degrees: float
property scale: simvx.core.math.types.Vec2
property world_position: simvx.core.math.types.Vec2
property world_rotation: float
property world_scale: simvx.core.math.types.Vec2
property forward: simvx.core.math.types.Vec2
property right: simvx.core.math.types.Vec2
translate(offset: tuple[float, float] | numpy.ndarray)
rotate(radians: float)
rotate_deg(degrees: float)
look_at(target: tuple[float, float] | numpy.ndarray)
transform_points(points: list[simvx.core.math.types.Vec2]) list[simvx.core.math.types.Vec2]
draw_polygon(renderer, points: list[simvx.core.math.types.Vec2], closed=True, colour=None)
wrap_screen(margin: float = 20)
strict_errors: ClassVar[bool]

True

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property process_mode: simvx.core.descriptors.ProcessMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
find_child(name: str, recursive: bool = False) simvx.core.node.Node | None
find(node_type: type, recursive: bool = True) simvx.core.node.Node | None
find_all(node_type: type, recursive: bool = True) list
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
ready() None
enter_tree() None
exit_tree() None
process(dt: float) None
physics_process(dt: float) None
picked(event: simvx.core.events.InputEvent) None
handle_input(event: simvx.core.events.TreeInputEvent) None
unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
clear_children()
destroy()
property app
property tree: simvx.core.scene_tree.SceneTree
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()
class simvx.editor.workspace_tabs.NewSceneDialog(**kwargs)[source]

Bases: simvx.core.Panel

Modal overlay for choosing a new scene root type.

Initialization

ROW_HEIGHT

40.0

DIALOG_WIDTH

320.0

HEADER_HEIGHT

36.0

show_dialog(parent_size: simvx.core.Vec2 | None = None)[source]

Show the dialog centered in the parent.

gui_input(event)[source]

Handle escape to dismiss.

style

‘ThemeStyleBox(…)’

property bg_colour
property border_colour
property border_width
get_minimum_size() simvx.core.math.types.Vec2
draw(renderer)
size_x

‘Property(…)’

size_y

‘Property(…)’

anchor_left

‘Property(…)’

anchor_top

‘Property(…)’

anchor_right

‘Property(…)’

anchor_bottom

‘Property(…)’

margin_left

‘Property(…)’

margin_top

‘Property(…)’

margin_right

‘Property(…)’

margin_bottom

‘Property(…)’

property size: simvx.core.math.types.Vec2
touch_mode: str

‘mouse’

property theme: simvx.core.ui.types.Theme | None
property mouse_over: bool
property focused: bool
property disabled: bool
get_theme() simvx.core.ui.types.Theme
queue_redraw()
get_rect() tuple[float, float, float, float]
get_global_rect() tuple[float, float, float, float]
is_point_inside(point) bool
set_anchor_preset(preset: simvx.core.ui.enums.AnchorPreset)
set_focus()
grab_focus()
release_focus()
has_focus() bool
focus_next_control()
focus_previous_control()
grab_mouse()
release_mouse()
set_drag_preview(control: simvx.core.ui.core.Control)
draw_popup(renderer)
is_popup_point_inside(point) bool
popup_input(event)
dismiss_popup()
z_index

‘Property(…)’

z_as_relative

‘Property(…)’

render_layer

‘Property(…)’

set_render_layer(index: int, enabled: bool = True) None
is_on_render_layer(index: int) bool
property absolute_z_index: int
property position: simvx.core.math.types.Vec2
property rotation: float
property rotation_degrees: float
property scale: simvx.core.math.types.Vec2
property world_position: simvx.core.math.types.Vec2
property world_rotation: float
property world_scale: simvx.core.math.types.Vec2
property forward: simvx.core.math.types.Vec2
property right: simvx.core.math.types.Vec2
translate(offset: tuple[float, float] | numpy.ndarray)
rotate(radians: float)
rotate_deg(degrees: float)
look_at(target: tuple[float, float] | numpy.ndarray)
transform_points(points: list[simvx.core.math.types.Vec2]) list[simvx.core.math.types.Vec2]
draw_polygon(renderer, points: list[simvx.core.math.types.Vec2], closed=True, colour=None)
wrap_screen(margin: float = 20)
strict_errors: ClassVar[bool]

True

script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property process_mode: simvx.core.descriptors.ProcessMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
find_child(name: str, recursive: bool = False) simvx.core.node.Node | None
find(node_type: type, recursive: bool = True) simvx.core.node.Node | None
find_all(node_type: type, recursive: bool = True) list
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
ready() None
enter_tree() None
exit_tree() None
process(dt: float) None
physics_process(dt: float) None
picked(event: simvx.core.events.InputEvent) None
handle_input(event: simvx.core.events.TreeInputEvent) None
unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
clear_children()
destroy()
property app
property tree: simvx.core.scene_tree.SceneTree
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
__repr__()