Skip to content

Add a runtime way to show/hide or load/unload a plugin in response to a trigger #147

@slesaad

Description

@slesaad

Add a runtime way to show/hide or load/unload a plugin in response to a trigger

Motivation

In the modern paneled layout, which plugins appear and which panels they live in is decided entirely up front, at configuration time. Once the app starts, the only control over a plugin is a static on/off in the config: a plugin is either present for the whole session or absent for the whole session. There is no way to react to anything that happens while the app is running.

That's too rigid for the kind of apps this framework is meant to assemble. Plugins should be able to respond to each other and to the core. A selection in one plugin, a mode change, a particular kind of data being loaded — any of these should be able to bring a relevant plugin into view or get an irrelevant one out of the way. Today that's impossible: everything that's going to be on screen is on screen from the first second, and everything that's hidden stays hidden forever. The result is cluttered apps and no way to guide a user's attention as they work.

How it should work

While the app is running, the core or any plugin can change another plugin's presence in response to an event. A plugin emits a signal ("a feature was selected", "analysis mode turned on"); something listening reacts. There are two flavors of reaction, and the caller chooses which one applies to a given plugin:

  • Show / hide — the target plugin stays loaded the whole time; hiding just takes it out of view. Because it's never torn down, its internal state survives: hide a plugin mid-task, show it again later, and it's exactly where the user left it, with no reload and no lost work. This is purely about visibility, and is the right choice for a plugin you toggle often or whose state must persist.

  • Load / unload — the target plugin is created on demand and fully torn down on demand. Before it's loaded it isn't running at all (no instance, no resources); a trigger brings it to life, and another trigger destroys it and releases what it held. Reloading it later starts it fresh. This is the right choice for a heavy or rarely-used plugin you don't want consuming anything until it's actually needed.

Either way the trigger is driven in code — a plugin or the core decides, in response to an event, to act on a plugin it names. A plugin can also start out hidden, or start out not-loaded, and be brought in later through the same mechanism. All of this works even when several plugins share one panel: acting on one of them leaves the others, and the panel itself, undisturbed.

Done when

  • At runtime, the core or a plugin can act on another plugin by name — without restarting the app or changing config.
  • Show/hide: a hidden plugin is removed from view but keeps its internal state; showing it again restores it as the user left it, with no re-initialization and no data loss.
  • Load/unload: a not-yet-loaded plugin can be created on demand by a trigger, and a loaded one fully destroyed by a trigger — releasing its resources and listeners (no leaks); a later load brings it up fresh.
  • The caller can choose, per plugin, whether a trigger does show/hide or load/unload.
  • Any of these can be driven by an event coming from another plugin or from the core — demonstrated end to end (an event fires → a plugin appears, disappears, loads, or unloads).
  • Within a panel that hosts more than one plugin, acting on a single plugin leaves the other plugins and the panel layout intact.
  • A plugin configured to start hidden — or to start not-loaded — can be brought in later through the same mechanism.
  • Repeated cycles (show→hide→show, load→unload→load) are stable — no duplicated UI, leftover artifacts, orphaned listeners, or growing memory.

Out of scope

  • A declarative config layer that maps events to show/hide/load/unload actions for non-coders. For now the wiring is done imperatively in code. A config-driven rules layer can build on top of this later.
  • Adding/removing panels or changing the panel layout at runtime.
  • The legacy (classic) layout — this targets the modern paneled UI only.
Draft implementation plan — written as of abef1d76 on 2026-06-15. Rough guide; re-verify against latest code.

Current behavior

  • The modern entry point brings up a panel-based layout. A panel manager (TypeScript singleton) holds panel state, and a separate modern UI renderer syncs that state to the DOM. Panel state is coarse — collapsed / iconified / focused / expanded — and operates at panel granularity, not per-plugin.
  • A modern tool controller assigns tools to panels (explicit panelTools list first, then a compatibility-based fallback) and calls each tool's make() once at startup. on: false in config skips a tool entirely. There is no per-plugin visibility toggle, and destroy() exists but is only wired for whole-panel unload / mission swap — there is no on-demand load/unload path keyed to a single plugin. (Confirmed by project note: "Modern UI has no tool show/hide or floating-tool support — tools auto-dock + make() once at startup.")
  • The frontend event bus is mmgisAPI (mitt-based): on / emit, a forPlugin(name) wrapper that namespaces emits/handlers as plugin:<name>:..., and a provide / request pair for async request/response. This is the natural seam for the imperative show/hide/load/unload calls and for the events that trigger them.

Where the change lands & rough plan

  • Add a runtime capability keyed by plugin name, exposed through mmgisAPI so both the core and any plugin can call it. Two operations:
    • show/hide — toggle a per-plugin DOM-container visibility inside the owning panel; do not call destroy(), so the instance and its state are preserved.
    • load/unload — call the plugin's make() on demand to create it, and destroy() to tear it down, coordinated with the tool controller / panel manager so the panel slot is populated or freed cleanly.
  • Let the caller pick the operation per plugin (e.g. a flag on the call or on the plugin's config entry).
  • Demonstrate the trigger path end to end: a plugin emits an event on the bus, a listener (core or another plugin) reacts by calling the relevant operation. This proves the "trigger from another plugin or the core" requirement without yet building a declarative config layer.
  • Handle start-hidden and start-not-loaded: the same code paths used at runtime should be reachable at init so a plugin can begin in either state and be brought in later.

⚠️ Gotcha: existing panel states are per-panel, not per-plugin. A panel can host several plugins, so acting on one plugin is not the same as collapsing the panel — collapsing would take the panel's other plugins down with it. This needs a per-plugin container concept distinct from panel state, plus care that the panel's space allocation / layout still behaves when one of several co-resident plugins is hidden or unloaded.

⚠️ Gotcha: make() is currently assumed to run exactly once at startup. On-demand load/unload makes it run again after a destroy(), so re-make() must be idempotent and destroy() must fully release listeners/resources — any startup-only assumptions baked into a tool's make() will surface here.

References

Snapshot-accurate only — re-verify:

  • src/essence/mmgisAPI/mmgisAPI.js — event bus, forPlugin, provide/request.
  • src/essence/Basics/PanelManager_/PanelManager_.ts — panel registry and state.
  • src/essence/Basics/UserInterface_/UserInterfaceModern_.js — modern DOM rendering / layout sync.
  • src/essence/Basics/ToolController_/ToolControllerModern_.js — tool→panel assignment and make()/destroy() lifecycle.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions