feat(plugin): enable/disable plugins from the Plugin Manager

The manager was a read-only inventory. Each plugin now has an
Enable/Disable toggle: a disabled plugin keeps its manifest listed but
drops its ribbon tab and command dispatch. The disabled-id set persists
in settings.txt (disabled_plugins=) and the ribbon rebuilds on toggle.

Part of the #100 extensibility epic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-17 10:15:38 +03:00
1 parent 195415af84
commit 844069b4ef
9 files changed
+157 -23

No files matched your search

+7
View File
@@ -293,6 +293,10 @@ pub(super) struct OpenCADStudio {
modal_drag_last: Option<Point>,
/// True while the modal title bar is held (a drag is in progress).
modal_dragging: bool,
/// Plugin ids the user turned off in the Plugin Manager. Disabled plugins
/// keep their manifest listed but drop their ribbon tab and command
/// dispatch. Persisted via [`settings::UserSettings::disabled_plugins`].
disabled_plugins: rustc_hash::FxHashSet<String>,
/// New-release notification window — opened on startup when the
/// GitHub releases API reports a newer version than this build.
/// First-launch "make Open CAD Studio the default for .dwg/.dxf?" prompt
@@ -1018,6 +1022,8 @@ pub enum Message {
PluginManagerOpen,
#[allow(dead_code)]
PluginManagerClose,
/// Enable (`true`) or disable (`false`) the plugin with this id.
SetPluginEnabled(String, bool),
// ── Quick Select / Select Similar ───────────────────────────────────
/// Extend the current selection with every entity in the active
/// layout that matches a selected entity by (type, layer).
@@ -1428,6 +1434,7 @@ impl OpenCADStudio {
modal_offset: iced::Vector::ZERO,
modal_drag_last: None,
modal_dragging: false,
disabled_plugins: rustc_hash::FxHashSet::default(),
default_assoc_prompted: false,
update_notice_version: None,
update_notice_body: None,
+15 -1
View File
@@ -74,6 +74,10 @@ pub struct UserSettings {
/// prompt has already been shown. Set once the user answers (either way),
/// so we never nag again on subsequent launches.
pub default_assoc_prompted: bool,
/// Ids of plugins the user turned off in the Plugin Manager. Disabled
/// plugins keep their manifest listed but drop their ribbon tab and command
/// dispatch.
pub disabled_plugins: Vec<String>,
}
impl Default for UserSettings {
@@ -96,6 +100,7 @@ impl Default for UserSettings {
SnapType::Nearest,
],
default_assoc_prompted: false,
disabled_plugins: Vec::new(),
}
}
}
@@ -130,6 +135,14 @@ impl UserSettings {
"osnap" => s.snap_enabled = val == "1",
"otrack" => s.otrack = val == "1",
"default_assoc_prompted" => s.default_assoc_prompted = val == "1",
"disabled_plugins" => {
s.disabled_plugins = val
.split(',')
.map(|t| t.trim())
.filter(|t| !t.is_empty())
.map(|t| t.to_string())
.collect();
}
"snap_modes" => {
let modes: Vec<SnapType> =
val.split(',').filter_map(|t| snap_from_id(t.trim())).collect();
@@ -155,7 +168,7 @@ impl UserSettings {
.collect::<Vec<_>>()
.join(",");
let body = format!(
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\ngrid={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\n",
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\ngrid={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\n",
b(self.dyn_input),
b(self.ortho),
b(self.polar),
@@ -165,6 +178,7 @@ impl UserSettings {
b(self.otrack),
b(self.default_assoc_prompted),
modes,
self.disabled_plugins.join(","),
);
let _ = std::fs::write(path, body);
}
+31
View File
@@ -141,6 +141,11 @@ impl OpenCADStudio {
otrack: self.snapper.otrack_enabled,
snap_modes: super::settings::UserSettings::modes_from(self.snapper.enabled.iter()),
default_assoc_prompted: self.default_assoc_prompted,
disabled_plugins: {
let mut v: Vec<String> = self.disabled_plugins.iter().cloned().collect();
v.sort();
v
},
}
}
@@ -155,6 +160,22 @@ impl OpenCADStudio {
self.snapper.otrack_enabled = s.otrack;
self.snapper.enabled = s.snap_modes.iter().copied().collect();
self.default_assoc_prompted = s.default_assoc_prompted;
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
self.rebuild_ribbon_modules();
}
/// Rebuild the ribbon's tab list from the registry, dropping the tabs of any
/// disabled plugins. Call after `disabled_plugins` changes.
pub(super) fn rebuild_ribbon_modules(&mut self) {
let modules =
crate::plugin::ribbon_modules_enabled(&self.disabled_plugins);
self.ribbon.set_modules(modules);
}
/// Snapshot of disabled plugin ids — lets the registry skip them while it
/// holds a `&mut` borrow of the app via `HostSession`.
pub(crate) fn disabled_plugin_ids(&self) -> rustc_hash::FxHashSet<String> {
self.disabled_plugins.clone()
}
/// Write preferences to disk only when they differ from the last write,
@@ -5432,6 +5453,16 @@ impl OpenCADStudio {
self.close_active_modal();
Task::none()
}
Message::SetPluginEnabled(id, enabled) => {
if enabled {
self.disabled_plugins.remove(&id);
} else {
self.disabled_plugins.insert(id);
}
self.rebuild_ribbon_modules();
self.persist_settings_if_changed();
Task::none()
}
Message::EnterViewport(handle) => {
let i = self.active_tab;
+4 -1
View File
@@ -1120,7 +1120,10 @@ impl OpenCADStudio {
sized(crate::ui::shortcuts::view_window(&self.shortcut_overrides), 720, 520)
}
super::ModalKind::PluginManager => sized(
crate::ui::plugin_manager::view_window(&crate::plugin::installed_manifests()),
crate::ui::plugin_manager::view_window(
&crate::plugin::installed_manifests(),
&self.disabled_plugins,
),
520,
460,
),