diff --git a/api/src/runtime/info_event.rs b/api/src/runtime/global_info_event.rs similarity index 93% rename from api/src/runtime/info_event.rs rename to api/src/runtime/global_info_event.rs index 3bb8c9703..1dd1c2937 100644 --- a/api/src/runtime/info_event.rs +++ b/api/src/runtime/global_info_event.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] #[serde(tag = "kind")] -pub enum InfoEvent { +pub enum GlobalInfoEvent { AutoAddedController(AutoAddedControllerEvent), PlaytimeActivationSucceeded, PlaytimeActivationFailed, diff --git a/api/src/runtime/instance_info_event.rs b/api/src/runtime/instance_info_event.rs new file mode 100644 index 000000000..9e6ce31a2 --- /dev/null +++ b/api/src/runtime/instance_info_event.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum InstanceInfoEvent { + /// If attempting to MIDI-learn but the track is either not armed or the input monitoring mode + /// is not suitable. + MidiLearnFromFxInputButTrackNotArmed, + MidiLearnFromFxInputButTrackHasAudioInput, +} diff --git a/api/src/runtime/mod.rs b/api/src/runtime/mod.rs index ee990188e..ddf4288eb 100644 --- a/api/src/runtime/mod.rs +++ b/api/src/runtime/mod.rs @@ -1,8 +1,11 @@ mod preset; pub use preset::*; -mod info_event; -pub use info_event::*; +mod global_info_event; +pub use global_info_event::*; + +mod instance_info_event; +pub use instance_info_event::*; mod reaper; pub use reaper::*; diff --git a/main/src/application/unit_model.rs b/main/src/application/unit_model.rs index 363df75c2..0fd446fd2 100644 --- a/main/src/application/unit_model.rs +++ b/main/src/application/unit_model.rs @@ -13,11 +13,11 @@ use crate::domain::{ CompartmentSettings, CompoundMappingSource, ControlContext, ControlInput, DomainEvent, DomainEventHandler, ExtendedProcessorContext, FeedbackAudioHookTask, FeedbackOutput, FeedbackRealTimeTask, FinalSourceFeedbackValue, GroupId, GroupKey, IncomingCompoundSourceValue, - InfoEvent, InputDescriptor, InstanceId, LastTouchedTargetFilter, MainMapping, MappingId, - MappingKey, MappingMatchedEvent, MessageCaptureEvent, MidiControlInput, NormalMainTask, - OscFeedbackTask, ParamSetting, PluginParams, ProcessorContext, ProjectionFeedbackValue, - QualifiedMappingId, RealearnControlSurfaceMainTask, RealearnTarget, ReaperTarget, - ReaperTargetType, SharedInstance, SharedUnit, SourceFeedbackEvent, + InputDescriptor, InstanceId, InternalInfoEvent, LastTouchedTargetFilter, MainMapping, + MappingId, MappingKey, MappingMatchedEvent, MessageCaptureEvent, MidiControlInput, + NormalMainTask, OscFeedbackTask, ParamSetting, PluginParams, ProcessorContext, + ProjectionFeedbackValue, QualifiedMappingId, RealearnControlSurfaceMainTask, RealearnTarget, + ReaperTarget, ReaperTargetType, SharedInstance, SharedUnit, SourceFeedbackEvent, StayActiveWhenProjectInBackground, Tag, TargetControlEvent, TargetTouchEvent, TargetValueChangedEvent, Unit, UnitContainer, UnitId, VirtualControlElementId, VirtualFx, VirtualSource, VirtualSourceValue, LUA_MIDI_SCRIPT_SOURCE_RUNTIME_NAME, @@ -26,7 +26,7 @@ use base::{Global, NamedChannelSender, SenderToNormalThread, SenderToRealTimeThr use derivative::Derivative; use enum_map::EnumMap; -use reaper_high::Reaper; +use reaper_high::{Reaper, Track}; use rx_util::Notifier; use rxrust::prelude::*; use slog::{debug, trace}; @@ -41,7 +41,8 @@ use itertools::Itertools; use realearn_api::persistence::{ FxDescriptor, MappingModification, TargetTouchCause, TrackDescriptor, }; -use reaper_medium::RecordingInput; +use realearn_api::runtime::{GlobalInfoEvent, InstanceInfoEvent}; +use reaper_medium::{InputMonitoringMode, RecordingInput}; use std::error::Error; use std::fmt; use std::rc::{Rc, Weak}; @@ -58,7 +59,8 @@ pub trait SessionUi { fn mapping_matched(&self, event: MappingMatchedEvent); fn handle_target_control(&self, event: TargetControlEvent); fn handle_source_feedback(&self, event: SourceFeedbackEvent); - fn handle_info_event(&self, event: &InfoEvent); + fn handle_internal_info_event(&self, event: &InternalInfoEvent); + fn handle_external_info_event(&self, event: InstanceInfoEvent); fn handle_everything_changed(&self, unit_model: &UnitModel); fn handle_unit_name_changed(&self); fn handle_global_control_and_feedback_state_changed(&self); @@ -1741,6 +1743,25 @@ impl UnitModel { reenable_control_after_touched: bool, ignore_sources: Vec, ) -> Result<(), &'static str> { + // Warn if settings are not good + if self.control_input.get().is_midi_fx_input() { + if let Some(track) = self.processor_context.track() { + if !track + .recording_input() + .is_some_and(|i| matches!(i, RecordingInput::Midi { .. })) + { + self.ui().handle_external_info_event( + InstanceInfoEvent::MidiLearnFromFxInputButTrackHasAudioInput, + ); + } else if !track.is_armed(false) + || track.input_monitoring_mode() != InputMonitoringMode::Normal + { + self.ui().handle_external_info_event( + InstanceInfoEvent::MidiLearnFromFxInputButTrackNotArmed, + ); + } + } + } let allow_virtual_sources = mapping_id.compartment != CompartmentKind::Controller; let osc_arg_index_hint = { let mapping = self @@ -2695,7 +2716,7 @@ impl DomainEventHandler for WeakUnitModel { match event { Info(evt) => { let s = session.try_borrow()?; - s.ui().handle_info_event(evt); + s.ui().handle_internal_info_event(evt); } ConditionsChanged => { let s = session.try_borrow()?; diff --git a/main/src/domain/control_surface.rs b/main/src/domain/control_surface.rs index 78788dd86..94c551948 100644 --- a/main/src/domain/control_surface.rs +++ b/main/src/domain/control_surface.rs @@ -264,7 +264,7 @@ impl RealearnControlSurfaceMiddleware { helgoboss_allocator::undesired_allocation_count(); if current_undesired_allocation_count != self.last_undesired_allocation_count { self.last_undesired_allocation_count = current_undesired_allocation_count; - let event = &crate::domain::InfoEvent::UndesiredAllocationCountChanged; + let event = &crate::domain::InternalInfoEvent::UndesiredAllocationCountChanged; for p in self.main_processors.borrow_mut().iter_mut() { p.process_info_event(event); } diff --git a/main/src/domain/eventing.rs b/main/src/domain/eventing.rs index 20f0df8fb..379a3114d 100644 --- a/main/src/domain/eventing.rs +++ b/main/src/domain/eventing.rs @@ -1,6 +1,6 @@ use crate::domain::{ CompartmentKind, CompoundMappingTarget, ControlLogContext, ControlLogEntry, FeedbackLogEntry, - InfoEvent, MappingId, MessageCaptureResult, PluginParamIndex, PluginParams, + InternalInfoEvent, MappingId, MessageCaptureResult, PluginParamIndex, PluginParams, ProjectionFeedbackValue, QualifiedMappingId, RawParamValue, }; use base::hash_util::NonCryptoHashSet; @@ -22,7 +22,7 @@ pub enum DomainEvent<'a> { }, UpdatedAllParameters(PluginParams), TargetValueChanged(TargetValueChangedEvent<'a>), - Info(&'a InfoEvent), + Info(&'a InternalInfoEvent), ProjectionFeedback(ProjectionFeedbackValue), MappingMatched(MappingMatchedEvent), HandleTargetControl(TargetControlEvent), diff --git a/main/src/domain/info_event.rs b/main/src/domain/internal_info_event.rs similarity index 65% rename from main/src/domain/info_event.rs rename to main/src/domain/internal_info_event.rs index 849cc8357..bea7b5da0 100644 --- a/main/src/domain/info_event.rs +++ b/main/src/domain/internal_info_event.rs @@ -1,4 +1,4 @@ #[derive(Debug)] -pub enum InfoEvent { +pub enum InternalInfoEvent { UndesiredAllocationCountChanged, } diff --git a/main/src/domain/io.rs b/main/src/domain/io.rs index b637c9039..5ff604cf6 100644 --- a/main/src/domain/io.rs +++ b/main/src/domain/io.rs @@ -36,6 +36,10 @@ impl ControlInput { pub fn is_midi_device(self) -> bool { matches!(self, ControlInput::Midi(MidiControlInput::Device(_))) } + + pub fn is_midi_fx_input(self) -> bool { + matches!(self, ControlInput::Midi(MidiControlInput::FxInput)) + } } impl Default for ControlInput { diff --git a/main/src/domain/main_processor.rs b/main/src/domain/main_processor.rs index 0547981f0..e98fd31cc 100644 --- a/main/src/domain/main_processor.rs +++ b/main/src/domain/main_processor.rs @@ -9,7 +9,7 @@ use crate::domain::{ FeedbackLogEntry, FeedbackOutput, FeedbackRealTimeTask, FeedbackResolution, FeedbackSendBehavior, FinalRealFeedbackValue, FinalSourceFeedbackValue, GlobalControlAndFeedbackState, GroupId, HitInstructionContext, HitInstructionResponse, - InfoEvent, InstanceId, IoUpdatedEvent, KeyMessage, MainMapping, MainSourceMessage, + InstanceId, InternalInfoEvent, IoUpdatedEvent, KeyMessage, MainMapping, MainSourceMessage, MappingActivationEffect, MappingControlResult, MappingId, MappingInfo, MessageCaptureEvent, MessageCaptureResult, MidiControlInput, MidiDestination, MidiScanResult, NoopLogger, NormalRealTimeTask, OrderedMappingIdSet, OrderedMappingMap, OscDeviceId, OscFeedbackTask, @@ -1847,7 +1847,7 @@ impl MainProcessor { && self.basics.settings.control_input == ControlInput::Osc(*device_id) } - pub fn process_info_event(&mut self, evt: &InfoEvent) { + pub fn process_info_event(&mut self, evt: &InternalInfoEvent) { self.basics .event_handler .handle_event_ignoring_error(DomainEvent::Info(evt)); diff --git a/main/src/domain/mod.rs b/main/src/domain/mod.rs index a7d782cf6..ef81636e7 100644 --- a/main/src/domain/mod.rs +++ b/main/src/domain/mod.rs @@ -150,8 +150,8 @@ pub use lua_support::*; mod lua_module_container; pub use lua_module_container::*; -mod info_event; -pub use info_event::*; +mod internal_info_event; +pub use internal_info_event::*; mod instance; pub use instance::*; diff --git a/main/src/infrastructure/plugin/backbone_shell.rs b/main/src/infrastructure/plugin/backbone_shell.rs index f6e1ceeae..1d48afbed 100644 --- a/main/src/infrastructure/plugin/backbone_shell.rs +++ b/main/src/infrastructure/plugin/backbone_shell.rs @@ -67,7 +67,7 @@ use realearn_api::persistence::{ FxDescriptor, MidiControllerConnection, MidiInputPort, MidiOutputPort, TargetTouchCause, TrackDescriptor, TrackFxChain, }; -use realearn_api::runtime::{AutoAddedControllerEvent, InfoEvent}; +use realearn_api::runtime::{AutoAddedControllerEvent, GlobalInfoEvent}; use reaper_high::{ ChangeEvent, CrashInfo, Fx, Guid, MiddlewareControlSurface, Project, Reaper, Track, }; @@ -2608,9 +2608,9 @@ impl LicenseManagerEventHandler for BackboneLicenseManagerEventHandler { playtime_clip_engine::ClipEngine::get().handle_changed_licenses(source.licenses()); // Send a notification to the app (if it wants to display "success") let info_event = if success { - InfoEvent::PlaytimeActivationSucceeded + GlobalInfoEvent::PlaytimeActivationSucceeded } else { - InfoEvent::PlaytimeActivationFailed + GlobalInfoEvent::PlaytimeActivationFailed }; shell.proto_hub().notify_about_global_info_event(info_event); // Give all Playtime instances a chance to load previously unloaded matrices @@ -2834,9 +2834,11 @@ async fn maybe_create_controller_for_device( .save_controller(controller)?; BackboneShell::get() .proto_hub() - .notify_about_global_info_event(InfoEvent::AutoAddedController(AutoAddedControllerEvent { - controller_id: outcome.id, - })); + .notify_about_global_info_event(GlobalInfoEvent::AutoAddedController( + AutoAddedControllerEvent { + controller_id: outcome.id, + }, + )); Ok(()) } diff --git a/main/src/infrastructure/plugin/unit_shell.rs b/main/src/infrastructure/plugin/unit_shell.rs index a6f74251c..7f3782f9f 100644 --- a/main/src/infrastructure/plugin/unit_shell.rs +++ b/main/src/infrastructure/plugin/unit_shell.rs @@ -148,7 +148,7 @@ impl UnitShell { ); let shared_unit_model = Rc::new(RefCell::new(unit_model)); let weak_session = Rc::downgrade(&shared_unit_model); - let unit_panel = UnitPanel::new(weak_session.clone(), instance_panel.clone()); + let unit_panel = UnitPanel::new(instance_id, weak_session.clone(), instance_panel.clone()); shared_unit_model .borrow_mut() .set_ui(Rc::downgrade(&unit_panel)); diff --git a/main/src/infrastructure/proto/ext.rs b/main/src/infrastructure/proto/ext.rs index 61a1e8225..c19bd89ca 100644 --- a/main/src/infrastructure/proto/ext.rs +++ b/main/src/infrastructure/proto/ext.rs @@ -30,6 +30,12 @@ use crate::infrastructure::server::data::get_controller_routing; use realearn_api::runtime::{ControllerPreset, LicenseInfo, MainPreset, ValidatedLicense}; impl occasional_instance_update::Update { + pub fn info_event(event: realearn_api::runtime::InstanceInfoEvent) -> Self { + let json = + serde_json::to_string(&event).expect("couldn't represent instance info event as JSON"); + Self::InfoEvent(json) + } + pub fn settings(instance_shell: &InstanceShell) -> Self { let settings = instance_shell.settings(); let json = @@ -66,9 +72,9 @@ impl occasional_global_update::Update { Self::ArrangementPlayState(ArrangementPlayState::from_engine(play_state).into()) } - pub fn info_event(event: realearn_api::runtime::InfoEvent) -> Self { + pub fn info_event(event: realearn_api::runtime::GlobalInfoEvent) -> Self { let json = - serde_json::to_string(&event).expect("couldn't represent main info event as JSON"); + serde_json::to_string(&event).expect("couldn't represent global info event as JSON"); Self::InfoEvent(json) } diff --git a/main/src/infrastructure/proto/generated.rs b/main/src/infrastructure/proto/generated.rs index 00afa3068..642d9f1f7 100644 --- a/main/src/infrastructure/proto/generated.rs +++ b/main/src/infrastructure/proto/generated.rs @@ -856,7 +856,7 @@ pub struct GetOccasionalInstanceUpdatesReply { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct OccasionalInstanceUpdate { - #[prost(oneof = "occasional_instance_update::Update", tags = "1, 2, 3")] + #[prost(oneof = "occasional_instance_update::Update", tags = "1, 2, 3, 4")] pub update: ::core::option::Option, } /// Nested message and enum types in `OccasionalInstanceUpdate`. @@ -873,6 +873,9 @@ pub mod occasional_instance_update { /// Everything within the instance has changed (e.g. instance data load). #[prost(bool, tag = "3")] EverythingHasChanged(bool), + /// Info event as JSON. + #[prost(string, tag = "4")] + InfoEvent(::prost::alloc::string::String), } } #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/main/src/infrastructure/proto/hub.rs b/main/src/infrastructure/proto/hub.rs index 88e1d5237..723c3fa6e 100644 --- a/main/src/infrastructure/proto/hub.rs +++ b/main/src/infrastructure/proto/hub.rs @@ -11,7 +11,7 @@ use crate::infrastructure::proto::{ OccasionalInstanceUpdate, OccasionalInstanceUpdateBatch, OccasionalUnitUpdateBatch, ProtoRequestHandler, ProtoSenders, QualifiedOccasionalUnitUpdate, }; -use realearn_api::runtime::InfoEvent; +use realearn_api::runtime::{GlobalInfoEvent, InstanceInfoEvent}; use reaper_high::ChangeEvent; #[derive(Debug)] @@ -43,7 +43,17 @@ impl ProtoHub { )) } - pub fn notify_about_global_info_event(&self, info_event: InfoEvent) { + pub fn notify_about_instance_info_event( + &self, + instance_id: InstanceId, + info_event: InstanceInfoEvent, + ) { + self.send_occasional_instance_updates(instance_id, || { + [occasional_instance_update::Update::info_event(info_event)] + }); + } + + pub fn notify_about_global_info_event(&self, info_event: GlobalInfoEvent) { self.send_occasional_global_updates(|| { [occasional_global_update::Update::info_event(info_event)] }); diff --git a/main/src/infrastructure/ui/unit_panel.rs b/main/src/infrastructure/ui/unit_panel.rs index 162e62d5b..0810efecc 100644 --- a/main/src/infrastructure/ui/unit_panel.rs +++ b/main/src/infrastructure/ui/unit_panel.rs @@ -15,8 +15,9 @@ use crate::application::{ use crate::base::when; use crate::domain::ui_util::format_tags_as_csv; use crate::domain::{ - CompartmentKind, InfoEvent, MappingId, MappingMatchedEvent, ProjectionFeedbackValue, - QualifiedMappingId, SourceFeedbackEvent, TargetControlEvent, TargetValueChangedEvent, + CompartmentKind, InstanceId, InternalInfoEvent, MappingId, MappingMatchedEvent, + ProjectionFeedbackValue, QualifiedMappingId, SourceFeedbackEvent, TargetControlEvent, + TargetValueChangedEvent, }; use crate::infrastructure::plugin::{update_auto_units_async, BackboneShell}; use crate::infrastructure::server::http::{ @@ -27,6 +28,8 @@ use crate::infrastructure::ui::util::{header_panel_height, parse_tags_from_csv}; use anyhow::Context; use base::{Global, SoundPlayer}; use helgoboss_allocator::undesired_allocation_count; +use playtime_api::runtime::InfoEvent; +use realearn_api::runtime::{GlobalInfoEvent, InstanceInfoEvent}; use rxrust::prelude::*; use std::rc::{Rc, Weak}; use swell_ui::{DialogUnits, Point, SharedView, View, ViewContext, WeakView, Window}; @@ -37,6 +40,7 @@ type _MainPanel = UnitPanel; /// The complete ReaLearn panel containing everything. #[derive(Debug)] pub struct UnitPanel { + instance_id: InstanceId, view: ViewContext, session: WeakUnitModel, instance_panel: WeakView, @@ -49,6 +53,7 @@ pub struct UnitPanel { impl UnitPanel { pub fn new( + instance_id: InstanceId, session: WeakUnitModel, instance_panel: WeakView, ) -> SharedView { @@ -56,6 +61,7 @@ impl UnitPanel { let panel_manager = Rc::new(RefCell::new(panel_manager)); let state = SharedMainState::default(); let main_panel = Self { + instance_id, view: Default::default(), state: state.clone(), session: session.clone(), @@ -309,17 +315,23 @@ impl UnitPanel { self.handle_affected_own(affected); } - fn handle_info_event(self: SharedView, event: &InfoEvent) { + fn handle_internal_info_event(self: SharedView, event: &InternalInfoEvent) { if !self.is_open() { return; } match event { - InfoEvent::UndesiredAllocationCountChanged => { + InternalInfoEvent::UndesiredAllocationCountChanged => { self.invalidate_status_2_text(); } } } + fn handle_external_info_event(self: SharedView, event: InstanceInfoEvent) { + BackboneShell::get() + .proto_hub() + .notify_about_instance_info_event(self.instance_id, event); + } + fn handle_unit_name_changed(&self) { if let Ok(shell) = self.instance_panel().shell() { // At the time when this method is called, the unit is still borrowed, so the proto hub can't create @@ -563,8 +575,12 @@ impl SessionUi for Weak { upgrade_panel(self).handle_affected(affected, initiator); } - fn handle_info_event(&self, event: &InfoEvent) { - upgrade_panel(self).handle_info_event(event); + fn handle_internal_info_event(&self, event: &InternalInfoEvent) { + upgrade_panel(self).handle_internal_info_event(event); + } + + fn handle_external_info_event(&self, event: InstanceInfoEvent) { + upgrade_panel(self).handle_external_info_event(event); } fn handle_everything_changed(&self, unit_model: &UnitModel) {