diff --git a/main/src/infrastructure/ui/app/app_panel.rs b/main/src/infrastructure/ui/app/app_panel.rs index dccc5147f..532139a0a 100644 --- a/main/src/infrastructure/ui/app/app_panel.rs +++ b/main/src/infrastructure/ui/app/app_panel.rs @@ -6,7 +6,10 @@ use anyhow::{anyhow, Context, Result}; use base::Global; use c_str_macro::c_str; use playtime_clip_engine::proto; -use playtime_clip_engine::proto::{reply, ClipEngineReceivers, EventReply, Reply}; +use playtime_clip_engine::proto::{ + event_reply, occasional_matrix_update, reply, ClipEngineReceivers, Empty, EventReply, + GetOccasionalMatrixUpdatesReply, OccasionalMatrixUpdate, Reply, +}; use prost::Message; use reaper_low::{raw, Swell}; use std::cell::RefCell; @@ -28,7 +31,7 @@ struct OpenState { app_handle: AppHandle, app_callback: Option, // TODO-medium This is too specific. - event_receivers: ClipEngineReceivers, + event_receivers: Option, } impl AppPanel { @@ -65,11 +68,19 @@ impl AppPanel { // Handshake finished! The app has the host callback and we have the app callback. open_state.app_callback = Some(callback); // Now we can start passing events to the app callback + self.start_timer(); + } + + fn start_timer(&self) { self.view .require_window() .set_timer(TIMER_ID, Duration::from_millis(30)); } + fn stop_timer(&self) { + self.view.require_window().kill_timer(TIMER_ID); + } + fn open_internal(&self, window: Window) -> Result<()> { let app_library = App::get_or_load_app_library()?; let session_id = self @@ -83,7 +94,7 @@ impl AppPanel { let open_state = OpenState { app_handle, app_callback: None, - event_receivers: App::get().clip_engine_hub().senders().subscribe_to_all(), + event_receivers: Some(subscribe_to_events()), }; *self.open_state.borrow_mut() = Some(open_state); Ok(()) @@ -107,16 +118,15 @@ impl OpenState { } pub fn send_pending_events(&mut self, session_id: &str) { - let Some(app_callback) = self.app_callback else { + let (Some(app_callback), Some(event_receivers)) = (self.app_callback, &mut self.event_receivers) else { return; }; - self.event_receivers - .process_pending_updates(session_id, &|event_reply| { - let reply = Reply { - value: Some(reply::Value::EventReply(event_reply)), - }; - let _ = send_to_app(app_callback, &reply); - }); + event_receivers.process_pending_updates(session_id, &|event_reply| { + let reply = Reply { + value: Some(reply::Value::EventReply(event_reply)), + }; + let _ = send_to_app(app_callback, &reply); + }); } pub fn close_app(&self, window: Window) -> Result<()> { @@ -137,10 +147,49 @@ impl View for AppPanel { self.open_internal(window).is_ok() } + fn close_requested(self: SharedView) -> bool { + // Don't really close window (along with the app instance). Just hide it. It's a bit faster + // when next opening the window. + self.view.require_window().hide(); + true + } + fn closed(self: SharedView, window: Window) { self.close_internal(window).unwrap(); } + fn shown_or_hidden(self: SharedView, shown: bool) -> bool { + if shown { + // Send events to app again. + if let Some(open_state) = self.open_state.borrow_mut().as_mut() { + open_state.event_receivers = Some(subscribe_to_events()); + } else { + // We also get called when the window is first opened, *before* `opened` is called! + // In that case, `open_state` is not set yet. That's how we know it's the first opening, + // not a subsequent show. We don't need to do anything in that case. + return false; + } + // Start processing events again when shown + self.start_timer(); + // Send a reset event to the app. That's not necessary when the app is first + // shown because it resubscribes to everything on start anyway. But it's important for + // subsequent shows because the app was not aware that it was not fed with events while + // hidden. + let _ = self.send_to_app(&Reply { + value: Some(reply::Value::EventReply(EventReply { + value: Some(event_reply::Value::Reset(Empty {})), + })), + }); + } else { + // Don't process events while hidden + if let Some(open_state) = self.open_state.borrow_mut().as_mut() { + open_state.event_receivers = None; + } + self.stop_timer(); + } + true + } + #[allow(clippy::single_match)] fn button_clicked(self: SharedView, resource_id: u32) { match resource_id { @@ -212,3 +261,7 @@ fn send_to_app(app_callback: AppCallback, reply: &Reply) { /// Signature of the function that's used from the host in order to call the external app. pub type AppCallback = unsafe extern "C" fn(data: *const u8, length: i32); + +fn subscribe_to_events() -> ClipEngineReceivers { + App::get().clip_engine_hub().senders().subscribe_to_all() +} diff --git a/main/src/infrastructure/ui/header_panel.rs b/main/src/infrastructure/ui/header_panel.rs index 18c7da827..8b62f5fd3 100644 --- a/main/src/infrastructure/ui/header_panel.rs +++ b/main/src/infrastructure/ui/header_panel.rs @@ -80,7 +80,6 @@ pub struct HeaderPanel { group_panel: RefCell>>, extra_panel: RefCell>>, pot_browser_panel: RefCell>>, - app_panel: RefCell>>, is_invoked_programmatically: Cell, } @@ -101,7 +100,6 @@ impl HeaderPanel { group_panel: Default::default(), extra_panel: Default::default(), pot_browser_panel: Default::default(), - app_panel: Default::default(), is_invoked_programmatically: false.into(), } } @@ -141,7 +139,6 @@ impl HeaderPanel { close_child_panel_if_open(&self.group_panel); close_child_panel_if_open(&self.extra_panel); close_child_panel_if_open(&self.pot_browser_panel); - close_child_panel_if_open(&self.app_panel); } pub fn handle_changed_midi_devices(&self) { @@ -284,6 +281,7 @@ impl HeaderPanel { let main_preset_manager = main_preset_manager.borrow(); let text_from_clipboard = Rc::new(get_text_from_clipboard().unwrap_or_default()); let text_from_clipboard_clone = text_from_clipboard.clone(); + let app_is_open = self.panel_manager().borrow().app_panel_is_open(); let data_object_from_clipboard = if text_from_clipboard.is_empty() { None } else { @@ -589,7 +587,15 @@ impl HeaderPanel { MainMenuAction::ReloadAllPresets }), item("Open Pot Browser", || MainMenuAction::OpenPotBrowser), - item("Open App", || MainMenuAction::OpenApp), + item("Show App", || MainMenuAction::ShowApp), + item_with_opts( + "Close App", + ItemOpts { + enabled: app_is_open, + checked: false, + }, + || MainMenuAction::CloseApp, + ), separator(), menu( "Logging", @@ -754,9 +760,12 @@ impl HeaderPanel { MainMenuAction::OpenPotBrowser => { self.show_pot_browser(); } - MainMenuAction::OpenApp => { + MainMenuAction::ShowApp => { self.show_app(); } + MainMenuAction::CloseApp => { + self.close_app(); + } MainMenuAction::OpenPresetFolder => self.open_preset_folder(), MainMenuAction::SendFeedbackNow => self.session().borrow().send_all_feedback(), MainMenuAction::LogDebugInfo => self.log_debug_info(), @@ -2287,7 +2296,11 @@ impl HeaderPanel { } fn show_app(&self) { - self.panel_manager().borrow().open_app_panel(); + self.panel_manager().borrow().show_app_panel(); + } + + fn close_app(&self) { + self.panel_manager().borrow().close_app_panel(); } fn open_preset_folder(&self) { @@ -3001,7 +3014,8 @@ enum MainMenuAction { LinkToPreset(PresetLinkScope, FxId, String), ReloadAllPresets, OpenPotBrowser, - OpenApp, + ShowApp, + CloseApp, OpenPresetFolder, EditNewOscDevice, EditExistingOscDevice(OscDeviceId), diff --git a/main/src/infrastructure/ui/independent_panel_manager.rs b/main/src/infrastructure/ui/independent_panel_manager.rs index c24d9aaf3..924f935af 100644 --- a/main/src/infrastructure/ui/independent_panel_manager.rs +++ b/main/src/infrastructure/ui/independent_panel_manager.rs @@ -102,12 +102,25 @@ impl IndependentPanelManager { self.message_panel.clone().open(reaper_main_window()); } - pub fn open_app_panel(&self) { - let result = self.open_app_panel_internal(); + pub fn show_app_panel(&self) { + let result = self.show_app_panel_internal(); notification::notify_user_on_anyhow_error(result); } - fn open_app_panel_internal(&self) -> anyhow::Result<()> { + pub fn close_app_panel(&self) { + self.app_panel.clone().close(); + } + + pub fn app_panel_is_open(&self) -> bool { + self.app_panel.is_open() + } + + fn show_app_panel_internal(&self) -> anyhow::Result<()> { + if let Some(window) = self.app_panel.view_context().window() { + // If window already open (and maybe just hidden), simply show it. + window.show(); + return Ok(()); + } // Fail fast if library not available let _ = App::get_or_load_app_library()?; // Then open @@ -154,6 +167,7 @@ impl IndependentPanelManager { p.close() } self.mapping_panels.clear(); + self.app_panel.close(); } fn request_panel(&mut self) -> SharedView { diff --git a/swell-ui/src/view.rs b/swell-ui/src/view.rs index 23d4ba4f5..4a9e521cc 100644 --- a/swell-ui/src/view.rs +++ b/swell-ui/src/view.rs @@ -86,6 +86,13 @@ pub trait View: Debug { /// WM_DESTROY. fn closed(self: SharedView, _window: Window) {} + /// WM_SHOWWINDOW. + /// + /// Should return `true` if processed. + fn shown_or_hidden(self: SharedView, shown: bool) -> bool { + false + } + /// WM_COMMAND, HIWORD(wparam) == 0. fn button_clicked(self: SharedView, resource_id: u32) { let _ = resource_id; diff --git a/swell-ui/src/view_manager.rs b/swell-ui/src/view_manager.rs index 87caa616c..4316de429 100644 --- a/swell-ui/src/view_manager.rs +++ b/swell-ui/src/view_manager.rs @@ -183,6 +183,7 @@ unsafe extern "C" fn view_dialog_proc( // `SetWindowLong()` for return values with special meaning. keyboard_focus_desired.into() } + raw::WM_SHOWWINDOW => view.shown_or_hidden(wparam == 1).into(), raw::WM_DESTROY => { let view_context = view.view_context(); view_context.closed_subject.borrow_mut().next(());