diff --git a/main/src/domain/osc.rs b/main/src/domain/osc.rs index d226f07e2..91306ef9e 100644 --- a/main/src/domain/osc.rs +++ b/main/src/domain/osc.rs @@ -8,6 +8,7 @@ use std::error::Error; use std::io; use std::net::{Ipv4Addr, SocketAddrV4, ToSocketAddrs, UdpSocket}; use std::str::FromStr; +use uuid::Uuid; const OSC_BUFFER_SIZE: usize = 10_000; @@ -101,22 +102,13 @@ impl OscOutputDevice { /// /// This uniquely identifies an OSC device according to ReaLearn's device configuration. #[derive( - Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Serialize, DeserializeFromStr, + Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Serialize, Deserialize, )] -pub struct OscDeviceId(String); +#[serde(transparent)] +pub struct OscDeviceId(uuid::Uuid); -impl FromStr for OscDeviceId { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let trimmed = s.trim(); - if trimmed.is_empty() { - return Err("OSC device ID must not be empty"); - } - let valid_regex = regex!(r#"^[A-Za-z0-9_~-]+$"#); - if !valid_regex.is_match(trimmed) { - return Err("OSC device ID contains illegal characters"); - } - Ok(OscDeviceId(trimmed.to_owned())) +impl OscDeviceId { + pub fn random() -> OscDeviceId { + OscDeviceId(Uuid::new_v4()) } } diff --git a/main/src/infrastructure/data/osc_device_management.rs b/main/src/infrastructure/data/osc_device_management.rs index ab9d0b726..5cc2a176a 100644 --- a/main/src/infrastructure/data/osc_device_management.rs +++ b/main/src/infrastructure/data/osc_device_management.rs @@ -2,6 +2,7 @@ use crate::core::default_util::{bool_true, is_bool_true, is_default}; use crate::domain::{OscDeviceId, OscInputDevice, OscOutputDevice}; use crate::infrastructure::plugin::App; use derive_more::Display; +use rx_util::UnitEvent; use rxrust::prelude::*; use serde::{Deserialize, Serialize}; use std::cell::RefCell; @@ -16,7 +17,7 @@ pub type SharedOscDeviceManager = Rc>; #[derive(Debug)] pub struct OscDeviceManager { - devices: Vec, + config: OscDeviceConfig, changed_subject: LocalSubject<'static, (), ()>, osc_device_config_file_path: PathBuf, } @@ -24,11 +25,11 @@ pub struct OscDeviceManager { impl OscDeviceManager { pub fn new(osc_device_config_file_path: PathBuf) -> OscDeviceManager { let mut manager = OscDeviceManager { + config: Default::default(), osc_device_config_file_path, - devices: vec![], changed_subject: Default::default(), }; - let _ = manager.load().unwrap(); + let _ = manager.load(); manager } @@ -37,24 +38,39 @@ impl OscDeviceManager { .map_err(|_| "couldn't read OSC device config file".to_string())?; let config: OscDeviceConfig = serde_json::from_str(&json) .map_err(|e| format!("OSC device config file isn't valid. Details:\n\n{}", e))?; - self.devices = config.devices; + self.config = config; + Ok(()) + } + + fn save(&mut self) -> Result<(), String> { + fs::create_dir_all(&self.osc_device_config_file_path.parent().unwrap()) + .map_err(|_| "couldn't create OSC device config file parent directory")?; + let json = serde_json::to_string_pretty(&self.config) + .map_err(|_| "couldn't serialize OSC device config")?; + fs::write(&self.osc_device_config_file_path, json) + .map_err(|_| "couldn't write OSC devie config file")?; Ok(()) } pub fn devices(&self) -> impl Iterator + ExactSizeIterator { - self.devices.iter() + self.config.devices.iter() } pub fn find_index_by_id(&self, id: &OscDeviceId) -> Option { - self.devices.iter().position(|dev| dev.id() == id) + self.config.devices.iter().position(|dev| dev.id() == id) + } + + pub fn find_device_by_id(&self, id: &OscDeviceId) -> Option<&OscDevice> { + self.config.devices.iter().find(|dev| dev.id() == id) } pub fn find_device_by_index(&self, index: usize) -> Option<&OscDevice> { - self.devices.get(index) + self.config.devices.get(index) } pub fn connect_all_enabled_inputs(&mut self) -> Vec { - self.devices + self.config + .devices .iter_mut() .filter(|dev| dev.is_enabled_for_control()) .flat_map(|dev| dev.connect_input()) @@ -62,22 +78,50 @@ impl OscDeviceManager { } pub fn connect_all_enabled_outputs(&mut self) -> Vec { - self.devices + self.config + .devices .iter_mut() .filter(|dev| dev.is_enabled_for_feedback()) .flat_map(|dev| dev.connect_output()) .collect() } + + pub fn changed(&self) -> impl UnitEvent { + self.changed_subject.clone() + } + + pub fn add_device(&mut self, dev: OscDevice) -> Result<(), &'static str> { + self.config.devices.push(dev); + self.save_and_notify_changed(); + Ok(()) + } + + pub fn update_device(&mut self, dev: OscDevice) -> Result<(), &'static str> { + let mut old_dev = self + .config + .devices + .iter_mut() + .find(|d| d.id() == dev.id()) + .ok_or("couldn't find OSC device")?; + std::mem::replace(old_dev, dev); + self.save_and_notify_changed(); + Ok(()) + } + + fn save_and_notify_changed(&mut self) { + self.save(); + self.changed_subject.next(()); + } } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct OscDeviceConfig { #[serde(default)] devices: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct OscDevice { id: OscDeviceId, @@ -101,7 +145,7 @@ pub struct OscDevice { impl Default for OscDevice { fn default() -> Self { Self { - id: OscDeviceId::from_str(nanoid::nanoid!(8).as_str()).unwrap(), + id: OscDeviceId::random(), name: "".to_string(), is_enabled_for_control: true, is_enabled_for_feedback: true, @@ -212,6 +256,21 @@ impl OscDevice { } Connected } + pub fn set_name(&mut self, name: String) { + self.name = name; + } + + pub fn set_local_port(&mut self, local_port: Option) { + self.local_port = local_port; + } + + pub fn set_device_host(&mut self, device_host: Option) { + self.device_host = device_host; + } + + pub fn set_device_port(&mut self, device_port: Option) { + self.device_port = device_port; + } } #[derive(Display)] diff --git a/main/src/infrastructure/data/session_data.rs b/main/src/infrastructure/data/session_data.rs index 0c0485e27..1f5f19ce8 100644 --- a/main/src/infrastructure/data/session_data.rs +++ b/main/src/infrastructure/data/session_data.rs @@ -44,12 +44,12 @@ pub struct SessionData { send_feedback_only_if_armed: bool, /// `None` means "" #[serde(default, skip_serializing_if = "is_default")] - control_device_id: Option, + control_device_id: Option, /// /// - `None` means "\" /// - `Some("fx-output")` means "\" #[serde(default, skip_serializing_if = "is_default")] - feedback_device_id: Option, + feedback_device_id: Option, // Not set before 1.12.0-pre9 #[serde(default, skip_serializing_if = "is_default")] default_group: Option, @@ -69,6 +69,20 @@ pub struct SessionData { parameters: HashMap, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +enum ControlDeviceId { + Osc(OscDeviceId), + Midi(String), +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +enum FeedbackDeviceId { + Osc(OscDeviceId), + MidiOrFxOutput(String), +} + impl Default for SessionData { fn default() -> Self { use crate::application::session_defaults; @@ -115,23 +129,23 @@ impl SessionData { control_device_id: if let Some(osc_dev_id) = session.osc_input_device_id.get_ref().as_ref() { - Some(osc_dev_id.to_string()) + Some(ControlDeviceId::Osc(osc_dev_id.clone())) } else { use MidiControlInput::*; match session.midi_control_input.get() { FxInput => None, - Device(dev) => Some(dev.id().to_string()), + Device(dev) => Some(ControlDeviceId::Midi(dev.id().to_string())), } }, feedback_device_id: if let Some(osc_dev_id) = session.osc_output_device_id.get_ref().as_ref() { - Some(osc_dev_id.to_string()) + Some(FeedbackDeviceId::Osc(osc_dev_id.clone())) } else { use MidiFeedbackOutput::*; session.midi_feedback_output.get().map(|o| match o { - Device(dev) => dev.id().to_string(), - FxOutput => "fx-output".to_string(), + Device(dev) => FeedbackDeviceId::MidiOrFxOutput(dev.id().to_string()), + FxOutput => FeedbackDeviceId::MidiOrFxOutput("fx-output".to_owned()), }) }, default_group: Some(GroupModelData::from_model( @@ -174,41 +188,47 @@ impl SessionData { // Validation let (midi_control_input, osc_control_input) = match self.control_device_id.as_ref() { None => (MidiControlInput::FxInput, None), - Some(dev_id_string) => { - let raw_dev_id = dev_id_string.parse::(); - if let Ok(raw_dev_id) = raw_dev_id { - // MIDI - let dev_id: MidiInputDeviceId = raw_dev_id - .try_into() - .map_err(|_| "invalid MIDI input device ID")?; - (MidiControlInput::Device(MidiInputDevice::new(dev_id)), None) - } else { - // OSC - ( - MidiControlInput::FxInput, - Some(dev_id_string.parse::()?), - ) + Some(dev_id) => { + use ControlDeviceId::*; + match dev_id { + Midi(midi_dev_id_string) => { + let raw_midi_dev_id = midi_dev_id_string + .parse::() + .map_err(|_| "invalid MIDI input device ID")?; + let midi_dev_id: MidiInputDeviceId = raw_midi_dev_id + .try_into() + .map_err(|_| "MIDI input device ID out of range")?; + ( + MidiControlInput::Device(MidiInputDevice::new(midi_dev_id)), + None, + ) + } + Osc(osc_dev_id) => (MidiControlInput::FxInput, Some(osc_dev_id.clone())), } } }; let (midi_feedback_output, osc_feedback_output) = match self.feedback_device_id.as_ref() { None => (None, None), - Some(dev_id_string) => { - if dev_id_string == "fx-output" { - (Some(MidiFeedbackOutput::FxOutput), None) - } else { - let raw_dev_id = dev_id_string.parse::(); - if let Ok(raw_dev_id) = raw_dev_id { - // MIDI - let dev_id = MidiOutputDeviceId::new(raw_dev_id); + Some(dev_id) => { + use FeedbackDeviceId::*; + match dev_id { + MidiOrFxOutput(s) if s == "fx-output" => { + (Some(MidiFeedbackOutput::FxOutput), None) + } + MidiOrFxOutput(midi_dev_id_string) => { + let midi_dev_id = midi_dev_id_string + .parse::() + .map(MidiOutputDeviceId::new) + .map_err(|_| "invalid MIDI output device ID")?; ( - Some(MidiFeedbackOutput::Device(MidiOutputDevice::new(dev_id))), + Some(MidiFeedbackOutput::Device(MidiOutputDevice::new( + midi_dev_id, + ))), None, ) - } else { - // OSC - (None, Some(dev_id_string.parse::()?)) } + Osc(osc_dev_id) => (None, Some(osc_dev_id.clone())), + _ => return Err("unknown feedback device ID"), } } }; diff --git a/main/src/infrastructure/ui/header_panel.rs b/main/src/infrastructure/ui/header_panel.rs index f73ca0180..e3c22d489 100644 --- a/main/src/infrastructure/ui/header_panel.rs +++ b/main/src/infrastructure/ui/header_panel.rs @@ -30,12 +30,14 @@ use crate::infrastructure::plugin::{ }; use crate::infrastructure::ui::bindings::root; +use crate::infrastructure::ui::dialog_util::prompt_for; use crate::infrastructure::ui::{ add_firewall_rule, GroupFilter, GroupPanel, IndependentPanelManager, SharedIndependentPanelManager, SharedMainState, }; use crate::infrastructure::ui::{dialog_util, CompanionAppPresenter}; use std::cell::{Cell, RefCell}; +use std::net::Ipv4Addr; const OSC_INDEX_OFFSET: isize = 1000; @@ -292,7 +294,7 @@ impl HeaderPanel { } fn invalidate_all_controls(&self) { - self.invalidate_midi_control_input_combo_box(); + self.invalidate_control_input_combo_box(); self.invalidate_feedback_output_combo_box(); self.invalidate_compartment_combo_box(); self.invalidate_preset_controls(); @@ -305,7 +307,7 @@ impl HeaderPanel { self.invalidate_learn_many_button(); } - fn invalidate_midi_control_input_combo_box(&self) { + fn invalidate_control_input_combo_box(&self) { self.invalidate_control_input_combo_box_options(); self.invalidate_control_input_combo_box_value(); } @@ -1295,7 +1297,7 @@ impl HeaderPanel { view.invalidate_all_controls(); }); self.when(session.midi_control_input.changed(), |view| { - view.invalidate_midi_control_input_combo_box(); + view.invalidate_control_input_combo_box(); view.invalidate_let_matched_events_through_check_box(); view.invalidate_let_unmatched_events_through_check_box(); let shared_session = view.session(); @@ -1361,6 +1363,18 @@ impl HeaderPanel { .do_async(move |view, _| { view.invalidate_preset_controls(); }); + when( + App::get() + .osc_device_manager() + .borrow() + .changed() + .take_until(self.view.closed()), + ) + .with(Rc::downgrade(&self)) + .do_async(move |view, _| { + view.invalidate_control_input_combo_box(); + view.invalidate_feedback_output_combo_box(); + }); // TODO-medium This is lots of stuff done whenever changing just something small in a // mapping or group. Maybe micro optimization, I don't know. Alternatively we could // just set a dirty flag once something changed and reset it after saving! @@ -1567,13 +1581,13 @@ impl View for HeaderPanel { use swell_ui::menu_tree::*; let dev_manager = App::get().osc_device_manager(); let dev_manager = dev_manager.borrow(); - let mut entries = once(item("", || Reaper::get().show_console_msg("New"))).chain( + let mut entries = once(item("", || edit_new_osc_device())).chain( dev_manager.devices().map(|dev| { let dev_id = dev.id().clone(); menu( dev.name(), vec![ - item("Edit...", || Reaper::get().show_console_msg("New")), + item("Edit...", move || edit_existing_osc_device(&dev_id)), item("Remove", move || { Reaper::get().show_console_msg(format!("Remove {:?}", dev_id)) }), @@ -1811,3 +1825,65 @@ fn generate_osc_device_heading(device_count: usize) -> String { } ) } + +fn edit_new_osc_device() { + let dev = match edit_osc_device(OscDevice::default()) { + Ok(d) => d, + Err(EditOscDevError::Cancelled) => return, + res => res.unwrap(), + }; + App::get().osc_device_manager().borrow_mut().add_device(dev); +} + +fn edit_existing_osc_device(dev_id: &OscDeviceId) { + let dev = App::get() + .osc_device_manager() + .borrow() + .find_device_by_id(dev_id) + .unwrap() + .clone(); + let dev = match edit_osc_device(dev) { + Ok(d) => d, + Err(EditOscDevError::Cancelled) => return, + res => res.unwrap(), + }; + App::get() + .osc_device_manager() + .borrow_mut() + .update_device(dev); +} + +#[derive(Debug)] +enum EditOscDevError { + Cancelled, + Unexpected(&'static str), +} + +fn edit_osc_device(mut dev: OscDevice) -> Result { + let csv = Reaper::get() + .medium_reaper() + .get_user_inputs( + "ReaLearn", + 4, + "Name,Local port,Device host,Device port,separator=;", + format!( + "{};{};{};{}", + dev.name(), + dev.local_port().map(|p| p.to_string()).unwrap_or_default(), + dev.device_host().map(|a| a.to_string()).unwrap_or_default(), + dev.device_port().map(|p| p.to_string()).unwrap_or_default(), + ), + 512, + ) + .ok_or(EditOscDevError::Cancelled)?; + let splitted: Vec<_> = csv.to_str().split(";").collect(); + if let [name, local_port, device_host, device_port] = splitted.as_slice() { + dev.set_name(name.to_string()); + dev.set_local_port(local_port.parse::().ok()); + dev.set_device_host(device_host.parse::().ok()); + dev.set_device_port(device_port.parse::().ok()); + Ok(dev) + } else { + Err(EditOscDevError::Unexpected("couldn't split")) + } +}