forked from 1b-agent/opencadstudio-upstream
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:
1 parent
195415af84
commit
844069b4ef
9 files changed
+157
-23
No files matched your search
@@ -255,7 +255,8 @@ This mirrors QGIS: the application ships core menus; plugins add tabs/tools with
|
|||||||
- [x] Storm Sewer off `commands.rs` monolith
|
- [x] Storm Sewer off `commands.rs` monolith
|
||||||
- [x] Single registration (`plugin.toml` + `BuiltinPlugin::ribbon`)
|
- [x] Single registration (`plugin.toml` + `BuiltinPlugin::ribbon`)
|
||||||
- [~] Extract `ocs_plugin_api` crate — manifest + ribbon/`CadModule` done; `acadrust`-typed host surface pending
|
- [~] Extract `ocs_plugin_api` crate — manifest + ribbon/`CadModule` done; `acadrust`-typed host surface pending
|
||||||
- [x] Plugin manager UI stub (list installed, versions) — `PLUGINS` / `PLUGINMANAGER` command, or the Start-page "Plugins" button
|
- [x] Plugin manager UI (list installed, versions) — `PLUGINS` / `PLUGINMANAGER` command, or the Start-page "Plugins" button
|
||||||
|
- [x] Enable/disable plugins from the manager — a disabled plugin drops its ribbon tab and command dispatch; persisted in `settings.txt` (`disabled_plugins=`)
|
||||||
|
|
||||||
### Phase 2 — Dynamic loading (desktop)
|
### Phase 2 — Dynamic loading (desktop)
|
||||||
|
|
||||||
@@ -268,6 +269,7 @@ This mirrors QGIS: the application ships core menus; plugins add tabs/tools with
|
|||||||
|
|
||||||
- `libloading` + `#[no_mangle] extern "C" fn ocs_plugin_register() -> *const PluginVTable`
|
- `libloading` + `#[no_mangle] extern "C" fn ocs_plugin_register() -> *const PluginVTable`
|
||||||
- `api_version` compatibility gate at load time
|
- `api_version` compatibility gate at load time
|
||||||
|
- [x] Enable/disable in settings (like QGIS plugin manager) — landed early in phase 1
|
||||||
- Enable/disable in settings (like QGIS plugin manager)
|
- Enable/disable in settings (like QGIS plugin manager)
|
||||||
|
|
||||||
### Phase 3 — Interchange & QA
|
### Phase 3 — Interchange & QA
|
||||||
|
|||||||
@@ -293,6 +293,10 @@ pub(super) struct OpenCADStudio {
|
|||||||
modal_drag_last: Option<Point>,
|
modal_drag_last: Option<Point>,
|
||||||
/// True while the modal title bar is held (a drag is in progress).
|
/// True while the modal title bar is held (a drag is in progress).
|
||||||
modal_dragging: bool,
|
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
|
/// New-release notification window — opened on startup when the
|
||||||
/// GitHub releases API reports a newer version than this build.
|
/// GitHub releases API reports a newer version than this build.
|
||||||
/// First-launch "make Open CAD Studio the default for .dwg/.dxf?" prompt
|
/// First-launch "make Open CAD Studio the default for .dwg/.dxf?" prompt
|
||||||
@@ -1018,6 +1022,8 @@ pub enum Message {
|
|||||||
PluginManagerOpen,
|
PluginManagerOpen,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
PluginManagerClose,
|
PluginManagerClose,
|
||||||
|
/// Enable (`true`) or disable (`false`) the plugin with this id.
|
||||||
|
SetPluginEnabled(String, bool),
|
||||||
// ── Quick Select / Select Similar ───────────────────────────────────
|
// ── Quick Select / Select Similar ───────────────────────────────────
|
||||||
/// Extend the current selection with every entity in the active
|
/// Extend the current selection with every entity in the active
|
||||||
/// layout that matches a selected entity by (type, layer).
|
/// layout that matches a selected entity by (type, layer).
|
||||||
@@ -1428,6 +1434,7 @@ impl OpenCADStudio {
|
|||||||
modal_offset: iced::Vector::ZERO,
|
modal_offset: iced::Vector::ZERO,
|
||||||
modal_drag_last: None,
|
modal_drag_last: None,
|
||||||
modal_dragging: false,
|
modal_dragging: false,
|
||||||
|
disabled_plugins: rustc_hash::FxHashSet::default(),
|
||||||
default_assoc_prompted: false,
|
default_assoc_prompted: false,
|
||||||
update_notice_version: None,
|
update_notice_version: None,
|
||||||
update_notice_body: None,
|
update_notice_body: None,
|
||||||
|
|||||||
+15
-1
@@ -74,6 +74,10 @@ pub struct UserSettings {
|
|||||||
/// prompt has already been shown. Set once the user answers (either way),
|
/// prompt has already been shown. Set once the user answers (either way),
|
||||||
/// so we never nag again on subsequent launches.
|
/// so we never nag again on subsequent launches.
|
||||||
pub default_assoc_prompted: bool,
|
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 {
|
impl Default for UserSettings {
|
||||||
@@ -96,6 +100,7 @@ impl Default for UserSettings {
|
|||||||
SnapType::Nearest,
|
SnapType::Nearest,
|
||||||
],
|
],
|
||||||
default_assoc_prompted: false,
|
default_assoc_prompted: false,
|
||||||
|
disabled_plugins: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +135,14 @@ impl UserSettings {
|
|||||||
"osnap" => s.snap_enabled = val == "1",
|
"osnap" => s.snap_enabled = val == "1",
|
||||||
"otrack" => s.otrack = val == "1",
|
"otrack" => s.otrack = val == "1",
|
||||||
"default_assoc_prompted" => s.default_assoc_prompted = 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" => {
|
"snap_modes" => {
|
||||||
let modes: Vec<SnapType> =
|
let modes: Vec<SnapType> =
|
||||||
val.split(',').filter_map(|t| snap_from_id(t.trim())).collect();
|
val.split(',').filter_map(|t| snap_from_id(t.trim())).collect();
|
||||||
@@ -155,7 +168,7 @@ impl UserSettings {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
let body = format!(
|
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.dyn_input),
|
||||||
b(self.ortho),
|
b(self.ortho),
|
||||||
b(self.polar),
|
b(self.polar),
|
||||||
@@ -165,6 +178,7 @@ impl UserSettings {
|
|||||||
b(self.otrack),
|
b(self.otrack),
|
||||||
b(self.default_assoc_prompted),
|
b(self.default_assoc_prompted),
|
||||||
modes,
|
modes,
|
||||||
|
self.disabled_plugins.join(","),
|
||||||
);
|
);
|
||||||
let _ = std::fs::write(path, body);
|
let _ = std::fs::write(path, body);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ impl OpenCADStudio {
|
|||||||
otrack: self.snapper.otrack_enabled,
|
otrack: self.snapper.otrack_enabled,
|
||||||
snap_modes: super::settings::UserSettings::modes_from(self.snapper.enabled.iter()),
|
snap_modes: super::settings::UserSettings::modes_from(self.snapper.enabled.iter()),
|
||||||
default_assoc_prompted: self.default_assoc_prompted,
|
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.otrack_enabled = s.otrack;
|
||||||
self.snapper.enabled = s.snap_modes.iter().copied().collect();
|
self.snapper.enabled = s.snap_modes.iter().copied().collect();
|
||||||
self.default_assoc_prompted = s.default_assoc_prompted;
|
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,
|
/// Write preferences to disk only when they differ from the last write,
|
||||||
@@ -5432,6 +5453,16 @@ impl OpenCADStudio {
|
|||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
Task::none()
|
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) => {
|
Message::EnterViewport(handle) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|||||||
+4
-1
@@ -1120,7 +1120,10 @@ impl OpenCADStudio {
|
|||||||
sized(crate::ui::shortcuts::view_window(&self.shortcut_overrides), 720, 520)
|
sized(crate::ui::shortcuts::view_window(&self.shortcut_overrides), 720, 520)
|
||||||
}
|
}
|
||||||
super::ModalKind::PluginManager => sized(
|
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,
|
520,
|
||||||
460,
|
460,
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -8,6 +8,6 @@ pub mod host;
|
|||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
|
|
||||||
pub use registry::all_ribbon_modules;
|
pub use registry::{all_ribbon_modules, ribbon_modules_enabled};
|
||||||
pub(crate) use host::BuiltinPlugin;
|
pub(crate) use host::BuiltinPlugin;
|
||||||
pub(crate) use registry::{installed_manifests, try_dispatch};
|
pub(crate) use registry::{installed_manifests, try_dispatch};
|
||||||
+32
-2
@@ -28,11 +28,20 @@ pub fn installed_manifests() -> Vec<&'static PluginManifest> {
|
|||||||
manifests
|
manifests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Core ribbon tabs plus add-on tabs (sorted by `manifest.ribbon_order`).
|
/// Core ribbon tabs plus *every* add-on tab (sorted by `manifest.ribbon_order`).
|
||||||
pub fn all_ribbon_modules() -> Vec<Box<dyn CadModule>> {
|
pub fn all_ribbon_modules() -> Vec<Box<dyn CadModule>> {
|
||||||
|
ribbon_modules_enabled(&rustc_hash::FxHashSet::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core ribbon tabs plus add-on tabs whose plugin id is **not** in `disabled`
|
||||||
|
/// (sorted by `manifest.ribbon_order`). Used by the Plugin Manager toggle.
|
||||||
|
pub fn ribbon_modules_enabled(
|
||||||
|
disabled: &rustc_hash::FxHashSet<String>,
|
||||||
|
) -> Vec<Box<dyn CadModule>> {
|
||||||
let mut core = core_registry::all_modules();
|
let mut core = core_registry::all_modules();
|
||||||
let mut addons: Vec<(i32, Box<dyn CadModule>)> = all_plugins()
|
let mut addons: Vec<(i32, Box<dyn CadModule>)> = all_plugins()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|p| !disabled.contains(p.manifest().id))
|
||||||
.map(|p| (p.manifest().ribbon_order, p.ribbon()))
|
.map(|p| (p.manifest().ribbon_order, p.ribbon()))
|
||||||
.collect();
|
.collect();
|
||||||
addons.sort_by_key(|(order, _)| *order);
|
addons.sort_by_key(|(order, _)| *order);
|
||||||
@@ -40,10 +49,15 @@ pub fn all_ribbon_modules() -> Vec<Box<dyn CadModule>> {
|
|||||||
core
|
core
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try each plugin until one handles `cmd`. Returns true if handled.
|
/// Try each *enabled* plugin until one handles `cmd`. Returns true if handled.
|
||||||
|
/// Disabled plugins (toggled off in the Plugin Manager) are skipped.
|
||||||
pub(crate) fn try_dispatch(app: &mut OpenCADStudio, tab: usize, cmd: &str) -> bool {
|
pub(crate) fn try_dispatch(app: &mut OpenCADStudio, tab: usize, cmd: &str) -> bool {
|
||||||
|
let disabled = app.disabled_plugin_ids();
|
||||||
let mut host = HostSession::new(app, tab);
|
let mut host = HostSession::new(app, tab);
|
||||||
for plugin in all_plugins() {
|
for plugin in all_plugins() {
|
||||||
|
if disabled.contains(plugin.manifest().id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if plugin.dispatch(&mut host, cmd) {
|
if plugin.dispatch(&mut host, cmd) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -98,6 +112,22 @@ mod tests {
|
|||||||
assert_eq!(titles.len(), core.len() + all_plugins().len());
|
assert_eq!(titles.len(), core.len() + all_plugins().len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_plugin_drops_its_ribbon_tab() {
|
||||||
|
let mut disabled = rustc_hash::FxHashSet::default();
|
||||||
|
disabled.insert("opencad.demo_plugin".to_string());
|
||||||
|
let titles: Vec<&str> = ribbon_modules_enabled(&disabled)
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.title())
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
!titles.contains(&"Demo Plugin"),
|
||||||
|
"disabled plugin still present: {titles:?}"
|
||||||
|
);
|
||||||
|
// Only the add-on tab is dropped; core tabs stay.
|
||||||
|
assert_eq!(titles.len(), core_registry::all_modules().len());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn try_dispatch_routes_demo_command() {
|
fn try_dispatch_routes_demo_command() {
|
||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
|||||||
+53
-17
@@ -1,13 +1,14 @@
|
|||||||
//! Plugin Manager window — lists the add-ons compiled into this build.
|
//! Plugin Manager window — lists the add-ons compiled into this build and lets
|
||||||
//!
|
//! the user enable/disable each one. A disabled plugin keeps its manifest
|
||||||
//! Phase-1 stub: read-only inventory of installed plugins (name, version, id,
|
//! listed but drops its ribbon tab and command dispatch (persisted across
|
||||||
//! API level, description). Enable/disable and dynamic loading come with the
|
//! launches). Dynamic loading still comes with the phase-2 loader; see
|
||||||
//! phase-2 loader; see `docs/plugin-architecture.md`.
|
//! `docs/plugin-architecture.md`.
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::plugin::manifest::PluginManifest;
|
use crate::plugin::manifest::PluginManifest;
|
||||||
use iced::widget::{column, container, row, scrollable, text, Space};
|
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Theme};
|
use iced::{Background, Border, Color, Element, Fill, Theme};
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
// Register the command names for autocomplete.
|
// Register the command names for autocomplete.
|
||||||
inventory::submit!(crate::command::CommandRegistration {
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
@@ -70,15 +71,47 @@ fn badge<'a>(label: String) -> Element<'a, Message> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plugin_card<'a>(m: &PluginManifest) -> Element<'a, Message> {
|
fn toggle_button<'a>(id: &str, disabled: bool) -> Element<'a, Message> {
|
||||||
let header = row![
|
// Label shows the action the click performs.
|
||||||
text(m.name.to_string()).size(15).color(WHITE),
|
let (label, on, off) = if disabled {
|
||||||
Space::new().width(Fill),
|
("Enable", Color { r: 0.18, g: 0.5, b: 0.25, a: 1.0 }, Color { r: 0.22, g: 0.6, b: 0.3, a: 1.0 })
|
||||||
badge(format!("v{}", m.version)),
|
} else {
|
||||||
Space::new().width(8),
|
("Disable", Color { r: 0.4, g: 0.22, b: 0.22, a: 1.0 }, Color { r: 0.55, g: 0.28, b: 0.28, a: 1.0 })
|
||||||
badge(format!("API {}", m.api_version.major)),
|
};
|
||||||
]
|
let want_enabled = disabled; // clicking flips the state
|
||||||
.align_y(iced::Center);
|
let id_owned = id.to_string();
|
||||||
|
button(text(label).size(12).color(WHITE))
|
||||||
|
.padding([3, 12])
|
||||||
|
.on_press(Message::SetPluginEnabled(id_owned, want_enabled))
|
||||||
|
.style(move |_: &Theme, status| {
|
||||||
|
let bg = match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => off,
|
||||||
|
_ => on,
|
||||||
|
};
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(bg)),
|
||||||
|
text_color: WHITE,
|
||||||
|
border: Border { radius: 4.0.into(), ..Default::default() },
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plugin_card<'a>(m: &PluginManifest, disabled: bool) -> Element<'a, Message> {
|
||||||
|
let mut header = row![text(m.name.to_string()).size(15).color(WHITE)];
|
||||||
|
if disabled {
|
||||||
|
header = header.push(Space::new().width(8));
|
||||||
|
header = header.push(badge("Disabled".to_string()));
|
||||||
|
}
|
||||||
|
let header = header
|
||||||
|
.push(Space::new().width(Fill))
|
||||||
|
.push(badge(format!("v{}", m.version)))
|
||||||
|
.push(Space::new().width(8))
|
||||||
|
.push(badge(format!("API {}", m.api_version.major)))
|
||||||
|
.push(Space::new().width(10))
|
||||||
|
.push(toggle_button(m.id, disabled))
|
||||||
|
.align_y(iced::Center);
|
||||||
|
|
||||||
let id_line = text(m.id.to_string()).size(11).color(ACCENT);
|
let id_line = text(m.id.to_string()).size(11).color(ACCENT);
|
||||||
let desc = text(m.description.to_string()).size(12).color(DIM);
|
let desc = text(m.description.to_string()).size(12).color(DIM);
|
||||||
@@ -107,7 +140,10 @@ fn plugin_card<'a>(m: &PluginManifest) -> Element<'a, Message> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_window<'a>(plugins: &[&'static PluginManifest]) -> Element<'a, Message> {
|
pub fn view_window<'a>(
|
||||||
|
plugins: &[&'static PluginManifest],
|
||||||
|
disabled: &FxHashSet<String>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
let title = text("Installed Plugins").size(20).color(WHITE);
|
let title = text("Installed Plugins").size(20).color(WHITE);
|
||||||
let subtitle = text(format!(
|
let subtitle = text(format!(
|
||||||
"{} add-on{} compiled into this build",
|
"{} add-on{} compiled into this build",
|
||||||
@@ -124,7 +160,7 @@ pub fn view_window<'a>(plugins: &[&'static PluginManifest]) -> Element<'a, Messa
|
|||||||
} else {
|
} else {
|
||||||
let mut list = column![].spacing(10);
|
let mut list = column![].spacing(10);
|
||||||
for m in plugins {
|
for m in plugins {
|
||||||
list = list.push(plugin_card(m));
|
list = list.push(plugin_card(m, disabled.contains(m.id)));
|
||||||
}
|
}
|
||||||
scrollable(list.width(Fill)).height(Fill).into()
|
scrollable(list.width(Fill)).height(Fill).into()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -135,6 +135,17 @@ impl Ribbon {
|
|||||||
self.active = index;
|
self.active = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace the tab list (e.g. after a plugin is enabled/disabled in the
|
||||||
|
/// Plugin Manager). Clamps the active tab so it stays in range.
|
||||||
|
pub fn set_modules(&mut self, modules: Vec<Box<dyn CadModule>>) {
|
||||||
|
self.modules = modules;
|
||||||
|
if self.active >= self.modules.len() {
|
||||||
|
self.active = self.modules.len().saturating_sub(1);
|
||||||
|
}
|
||||||
|
self.active_tool = None;
|
||||||
|
self.open_dropdown = None;
|
||||||
|
}
|
||||||
pub fn activate_tool(&mut self, id: &str) {
|
pub fn activate_tool(&mut self, id: &str) {
|
||||||
self.active_tool = Some(id.to_string());
|
self.active_tool = Some(id.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user