diff --git a/main/src/infrastructure/plugin/backbone_shell.rs b/main/src/infrastructure/plugin/backbone_shell.rs index bc8d5f643..f844da418 100644 --- a/main/src/infrastructure/plugin/backbone_shell.rs +++ b/main/src/infrastructure/plugin/backbone_shell.rs @@ -44,7 +44,9 @@ use crate::base::notification::notify_user_about_anyhow_error; use crate::infrastructure::plugin::actions::ACTION_DEFS; use crate::infrastructure::plugin::api_impl::{register_api, unregister_api}; use crate::infrastructure::plugin::debug_util::resolve_symbols_from_clipboard; -use crate::infrastructure::plugin::dynamic_toolbar::add_or_remove_toolbar_button; +use crate::infrastructure::plugin::dynamic_toolbar::{ + add_or_remove_toolbar_button, custom_toolbar_api_is_available, ToolbarChangeDetector, +}; use crate::infrastructure::plugin::helgobox_plugin::HELGOBOX_UNIQUE_VST_PLUGIN_ADD_STRING; use crate::infrastructure::plugin::hidden_helper_panel::HiddenHelperPanel; use crate::infrastructure::plugin::persistent_toolbar::add_toolbar_button_persistently; @@ -194,6 +196,7 @@ pub struct BackboneShell { osc_feedback_processor: Rc>, proto_hub: crate::infrastructure::proto::ProtoHub, welcome_panel: RefCell>>, + toolbar_change_detector: Option>, /// We need to keep this panel in memory in order to be informed when it's destroyed. _shutdown_detection_panel: SharedView, } @@ -442,11 +445,30 @@ impl BackboneShell { // Must be called after registering actions and waking REAPER up, otherwise it won't find the command IDs. let _ = Self::register_extension_menu(); let _ = Self::register_toolbar_icon_map(); - for (key, value) in &config.toolbar { - if *value > 0 { - let _ = add_or_remove_toolbar_button(key, true); + let toolbar_change_detector = if custom_toolbar_api_is_available() { + // Auto-add previously enabled dynamic toolbar buttons, if not present already + for (command_name, enabled) in &config.toolbar { + if *enabled > 0 { + let _ = add_or_remove_toolbar_button(command_name, true); + } } - } + // Create change detector to automatically disable a dynamic toolbar button if the user removes the + // button manually. + let observed_commands = ACTION_DEFS.iter().filter_map(|def| { + if !def.add_toolbar_button { + return None; + } + let command_id = Reaper::get() + .action_by_command_name(def.command_name) + .command_id() + .ok()?; + Some((command_id, def.command_name.to_string())) + }); + let detector = ToolbarChangeDetector::new(observed_commands.collect()); + Some(RefCell::new(detector)) + } else { + None + }; // Detect shutdown via hidden child window as suggested by Justin let shutdown_detection_panel = SharedView::new(HiddenHelperPanel::new()); shutdown_detection_panel.clone().open(reaper_main_window()); @@ -479,6 +501,7 @@ impl BackboneShell { osc_feedback_processor: Rc::new(RefCell::new(osc_feedback_processor)), proto_hub: crate::infrastructure::proto::ProtoHub::new(), welcome_panel: Default::default(), + toolbar_change_detector, _shutdown_detection_panel: shutdown_detection_panel, } } @@ -1071,6 +1094,20 @@ impl BackboneShell { }) } + /// To be called regularly, maybe once a second. + /// + /// See https://github.com/helgoboss/helgobox/issues/1331. + pub fn disable_manually_removed_dynamic_toolbar_buttons(&self) { + let Some(detector) = &self.toolbar_change_detector else { + return; + }; + for command_name in detector.borrow_mut().detect_manually_removed_commands() { + self.change_config(|config| { + config.toolbar.insert(command_name.to_string(), 0); + }) + } + } + /// Logging debug info is always initiated by a particular session. pub fn log_debug_info(&self, session_id: &str) { let msg = format!( @@ -2159,6 +2196,7 @@ impl Drop for BackboneShell { #[serde(default)] pub struct BackboneConfig { main: MainConfig, + // Map from command name (e.g. HB_SHOW_HIDE_PLAYTIME to state integer, 0 for disabled , 1 for enabled) toolbar: HashMap, } diff --git a/main/src/infrastructure/plugin/dynamic_toolbar.rs b/main/src/infrastructure/plugin/dynamic_toolbar.rs index 5bd3ef9b0..3a4aa0f50 100644 --- a/main/src/infrastructure/plugin/dynamic_toolbar.rs +++ b/main/src/infrastructure/plugin/dynamic_toolbar.rs @@ -1,7 +1,17 @@ +use base::hash_util::{NonCryptoHashMap, NonCryptoHashSet}; use reaper_high::Reaper; use reaper_medium::{CommandId, MenuOrToolbarItem, PositionDescriptor, UiRefreshBehavior}; -/// Dynamically adds or removes a toolbar button without persisting it. +pub fn custom_toolbar_api_is_available() -> bool { + Reaper::get() + .medium_reaper() + .low() + .pointers() + .GetCustomMenuOrToolbarItem + .is_some() +} + +/// Dynamically adds or removes a toolbar button without persisting it, returning the command ID. /// /// Requires REAPER version >= 711+dev0305. /// @@ -12,7 +22,7 @@ use reaper_medium::{CommandId, MenuOrToolbarItem, PositionDescriptor, UiRefreshB /// # Panics /// /// Panics if the REAPER version is too low. -pub fn add_or_remove_toolbar_button(command_name: &str, add: bool) -> anyhow::Result<()> { +pub fn add_or_remove_toolbar_button(command_name: &str, add: bool) -> anyhow::Result { let action = Reaper::get().action_by_command_name(command_name); let command_id = action.command_id()?; let reaper = Reaper::get().medium_reaper(); @@ -40,7 +50,38 @@ pub fn add_or_remove_toolbar_button(command_name: &str, add: bool) -> anyhow::Re } } } - Ok(()) + Ok(command_id) +} + +#[derive(Debug)] +pub struct ToolbarChangeDetector { + /// Map from command ID to command name. + observed_commands: NonCryptoHashMap, + present_commands: NonCryptoHashSet, +} + +impl ToolbarChangeDetector { + pub fn new(observed_commands: NonCryptoHashMap) -> Self { + Self { + observed_commands, + present_commands: Default::default(), + } + } + + pub fn detect_manually_removed_commands(&mut self) -> Vec<&str> { + self.observed_commands + .iter() + .filter(|(command_id, _)| { + if scan_toolbar_for_command_id(**command_id).is_some() { + self.present_commands.insert(**command_id); + false + } else { + self.present_commands.remove(command_id) + } + }) + .map(|(_, command_name)| command_name.as_str()) + .collect() + } } fn scan_toolbar_for_command_id(command_id: CommandId) -> Option { diff --git a/main/src/infrastructure/plugin/hidden_helper_panel.rs b/main/src/infrastructure/plugin/hidden_helper_panel.rs index 3ebdd2688..8254d0d4b 100644 --- a/main/src/infrastructure/plugin/hidden_helper_panel.rs +++ b/main/src/infrastructure/plugin/hidden_helper_panel.rs @@ -8,7 +8,8 @@ pub struct HiddenHelperPanel { view: ViewContext, } -const TIMER_ID: usize = 322; +const PLAYTIME_ENGINE_STATS_TIMER_ID: usize = 322; +const HELGOBOX_TOOLBAR_CHECK_TIMER_ID: usize = 323; impl HiddenHelperPanel { pub fn new() -> Self { @@ -30,7 +31,8 @@ impl View for HiddenHelperPanel { } fn opened(self: SharedView, window: Window) -> bool { - window.set_timer(TIMER_ID, Duration::from_millis(200)); + window.set_timer(PLAYTIME_ENGINE_STATS_TIMER_ID, Duration::from_millis(200)); + window.set_timer(HELGOBOX_TOOLBAR_CHECK_TIMER_ID, Duration::from_millis(3000)); false } @@ -39,15 +41,21 @@ impl View for HiddenHelperPanel { } fn timer(&self, id: usize) -> bool { - if id != TIMER_ID { - return false; + match id { + PLAYTIME_ENGINE_STATS_TIMER_ID => { + #[cfg(feature = "playtime")] + { + BackboneShell::get() + .proto_hub() + .notify_engine_stats_changed(); + } + true + } + HELGOBOX_TOOLBAR_CHECK_TIMER_ID => { + BackboneShell::get().disable_manually_removed_dynamic_toolbar_buttons(); + true + } + _ => false, } - #[cfg(feature = "playtime")] - { - BackboneShell::get() - .proto_hub() - .notify_engine_stats_changed() - } - true } } diff --git a/main/src/infrastructure/plugin/mod.rs b/main/src/infrastructure/plugin/mod.rs index 0573e23f6..711f7a46c 100644 --- a/main/src/infrastructure/plugin/mod.rs +++ b/main/src/infrastructure/plugin/mod.rs @@ -15,7 +15,7 @@ mod actions; mod ini_util; pub use actions::*; -mod dynamic_toolbar; +pub mod dynamic_toolbar; mod hidden_helper_panel; pub mod persistent_toolbar; mod sandbox; diff --git a/main/src/infrastructure/ui/welcome_panel.rs b/main/src/infrastructure/ui/welcome_panel.rs index 30539778c..928dbace8 100644 --- a/main/src/infrastructure/ui/welcome_panel.rs +++ b/main/src/infrastructure/ui/welcome_panel.rs @@ -1,9 +1,9 @@ use enumset::EnumSet; -use reaper_high::Reaper; use reaper_low::raw; use std::fmt::Debug; use crate::base::notification::alert; +use crate::infrastructure::plugin::dynamic_toolbar::custom_toolbar_api_is_available; use crate::infrastructure::plugin::{ BackboneShell, ACTION_SHOW_HIDE_PLAYTIME_COMMAND_NAME, ACTION_SHOW_WELCOME_SCREEN_LABEL, }; @@ -86,15 +86,6 @@ impl View for WelcomePanel { } } -fn custom_toolbar_api_is_available() -> bool { - Reaper::get() - .medium_reaper() - .low() - .pointers() - .GetCustomMenuOrToolbarItem - .is_some() -} - impl WelcomePanel { fn invalidate_controls(&self) { if custom_toolbar_api_is_available() {