0.4.7 — Tab Identity Refactor#

Released — current version

Every tab, pane, and window now carries a stable integer ID allocated at creation time from a single process-wide counter. Internal operations that previously looked up tabs by display label now key off tab_id, so multi-split layouts with duplicate screen names no longer confuse focus restoration, merge, or refresh.

Identity Module#

  • New VIStk.Objects._Identity.new_id() returns a fresh, strictly-increasing int; thread-safe.

  • IDs are unique for the lifetime of the Python process but not persisted across runs. Persistence (for “remember open tabs”) will use UUIDs when it lands in 0.6.X settings.

Tab IDs#

  • TabManager.open_tab(name, module, ...) now returns the new tab_id: int (was bool). Hold the ID to address a specific tab instance.

  • TabManager._tabs is keyed by tab_id (was the display label). Each entry carries its own display_name, base_name, and tab_id.

  • Public methods — close_tab, focus_tab, has_tab, force_refresh_tab, set_tab_info — accept either a tab_id (int) or a display label (str). Label lookups return the first match; prefer the tab_id returned by open_tab.

  • TabManager.active is now int | None (was str | None). Use TabManager.display_name(tab_id) to resolve back to a label.

  • Callbacks now pass tab_id first:

    manager.on_tab_activate    = lambda tab_id, module: ...
    manager.on_tab_deactivate  = lambda tab_id | None: ...
    manager.on_tab_popout      = lambda tab_id: ...
    manager.on_tab_detach      = lambda tab_id: ...
    manager.on_tab_refresh     = lambda tab_id: ...
    manager.on_tab_info_change = lambda tab_id, info: ...
    manager.on_tab_split       = lambda tab_id, direction, pane: ...
    
  • TabBar._tabs is keyed by tab_id with the label in entry["label"]. TabBar.update_tab_label(tab_id, new_label) replaces it.

Pane and Window IDs#

  • TabManager gains self.id: int.

  • _SplitNode gains self.id: int; SplitView._pane_parents is now keyed by .id rather than id(object), avoiding any theoretical memory-address reuse collisions.

  • DetachedWindow gains self.id: int (attribute only — no lookups are keyed off it yet; future “remember open windows” will consume it).

SplitView.remove_pane — Focus Restore Fix#

This is the primary bug fix for 0.4.7.

Previously, when a pane was removed the SplitView rebuilt the surviving subtree under a fresh Tk parent (because ttk.PanedWindow requires direct Tk children). After the rebuild, focus fell back to panes[0] — the first pane in left-to-right order. In multi-split layouts where multiple panes contained tabs with the same base name (for example two Dashboard tabs), the user’s focused pane could be silently lost.

Now:

  • Before destroying the old subtree, the SplitView records the tab_id of the currently active tab inside the surviving pane.

  • _snapshot_subtree captures each tab’s tab_id alongside its module, hooks, icon and info.

  • _rebuild_from_snapshot passes tab_id=... to TabManager.open_tab, so the reopened tabs keep their identities.

  • After rebuild, the SplitView finds whichever new pane now owns the recorded tab_id and restores focus there. If the focused pane was the one removed (or the ID could not be recovered), it falls back to the first surviving pane as before.

SplitView.find_pane_for_tab(tab_id) replaces the former name-based lookup.

Host#

  • _find_tab_by_base(base_name) now returns (tab_manager, tab_id) instead of (tab_manager, display_name).

  • _get_all_tab_names is renamed _get_all_tab_labels; it still collects display labels for _unique_display_name only.

  • _open_counts is retired — tab IDs make multi-instance tracking trivial, label uniqueness is purely a UX concern handled by _unique_display_name.

  • Screen.close() routes through the ID-based close_tab(tab_id).

IPC#

IPC is not being reintroduced. The draft 0.4.7 spec in the changelog mentioned __VIS_CLOSE__ but the in-process _HOST_INSTANCE singleton remains the sole navigation path. If IPC is added later it will use tab IDs natively.

Backward Compatibility#

  • TabManager public methods still accept display labels (str) as the key argument, so existing screen code calling tm.close_tab("Work Orders") continues to work. Prefer holding the tab_id returned by open_tab for deterministic lookup in the presence of duplicates.

  • The TabManager.open_tab return type changed from bool to int | None. Falsy None still indicates failure (tab already exists); non-None truthy int still indicates success — the if tm.open_tab(...): idiom keeps working.

  • TabManager.active is now int | None. Code comparing tm.active == "ScreenName" will need to call tm.display_name(tm.active) for the label.

Out of Scope#

  • Persistent (UUID / cross-process) IDs — deferred to 0.6.X.

  • Deregistering stale TabManager references from Host.registered_tab_managers and DetachedWindow.tab_managers after remove_pane — pre-existing bookkeeping leak, orthogonal to this refactor.