From 81d280bae8f0b3fb1ebea1a3350d008930c28927 Mon Sep 17 00:00:00 2001 From: Timofey Date: Fri, 18 Nov 2022 05:08:00 +0100 Subject: [PATCH] Medium-level API: Add various `ext_state` and corresponding changes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `set_ext_state` `get_ext_state` `has_ext_state` `delete_ext_state` `set_project_ext_state` `set_project_ext_state_unchecked` `get_project_ext_state` `get_project_ext_state_unchecked` `delete_project_ext_state` — verbose function to pass empty string. `delete_project_ext_state_unchecked` `enum_project_ext_state` with `EnumProjectExtStateResult` `enum_project_ext_state_unchecked` As `enum_project_ext_state` expects two string buffers, also introduced: `create_string_buffer` `create_buffer` Covered with tests. --- main/medium/src/reaper.rs | 266 +++++++++++++++++++++++++++++++++++++- main/medium/src/util.rs | 16 ++- test/test/src/tests.rs | 70 +++++++++- 3 files changed, 344 insertions(+), 8 deletions(-) diff --git a/main/medium/src/reaper.rs b/main/medium/src/reaper.rs index 95c8e1b9..79412d9b 100644 --- a/main/medium/src/reaper.rs +++ b/main/medium/src/reaper.rs @@ -33,7 +33,8 @@ use helgoboss_midi::ShortMessage; use reaper_low::raw::GUID; use crate::util::{ - create_passing_c_str, with_buffer, with_string_buffer, with_string_buffer_prefilled, + create_passing_c_str, create_string_buffer, with_buffer, with_string_buffer, + with_string_buffer_prefilled, }; use enumflags2::BitFlags; use std::fmt::Debug; @@ -6531,6 +6532,252 @@ impl Reaper { Ok(()) } + /// Set the extended state value for a specific section and key. + /// + /// persist=true means the value should be stored and reloaded + /// the next time REAPER is opened. + pub fn set_ext_state<'a>( + &self, + section: impl Into>, + key: impl Into>, + value: impl Into>, + persist: bool, + ) where + UsageScope: MainThreadOnly, + { + unsafe { + self.low().SetExtState( + section.into().as_ptr(), + key.into().as_ptr(), + value.into().as_ptr(), + persist, + ) + } + } + + /// Get the extended state value for a specific section and key. + pub fn get_ext_state<'a>( + &self, + section: impl Into>, + key: impl Into>, + ) -> ReaperString + where + UsageScope: MainThreadOnly, + { + unsafe { + let ptr = self + .low() + .GetExtState(section.into().as_ptr(), key.into().as_ptr()); + ReaperStr::from_ptr(ptr).to_reaper_string() + } + } + + /// Returns true if there exists an extended state value + /// for a specific section and key. + pub fn has_ext_state<'a>( + &self, + section: impl Into>, + key: impl Into>, + ) -> bool + where + UsageScope: MainThreadOnly, + { + unsafe { + self.low() + .HasExtState(section.into().as_ptr(), key.into().as_ptr()) + } + } + + /// Delete the extended state value for a specific section and key. + /// + /// persist=true means the value should remain deleted + /// the next time REAPER is opened + pub fn delete_ext_state<'a>( + &self, + section: impl Into>, + key: impl Into>, + persist: bool, + ) where + UsageScope: MainThreadOnly, + { + unsafe { + self.low() + .DeleteExtState(section.into().as_ptr(), key.into().as_ptr(), persist) + } + } + + /// Save a key/value pair for a specific extension, + /// to be restored the next time this specific project is loaded. + /// + /// Typically extname will be the name of a reascript or extension section. + /// + /// For idiomatic deleting project state use [`delete_project_ext_State`] + pub fn set_project_ext_state<'a>( + &self, + project: ProjectContext, + section: impl Into>, + key: impl Into>, + value: impl Into>, + ) -> bool + where + UsageScope: MainThreadOnly, + { + self.require_valid_project(project); + unsafe { self.set_project_ext_state_unchecked(project, section, key, value) } + } + + pub unsafe fn set_project_ext_state_unchecked<'a>( + &self, + project: ProjectContext, + section: impl Into>, + key: impl Into>, + value: impl Into>, + ) -> bool + where + UsageScope: MainThreadOnly, + { + self.low().SetProjExtState( + project.to_raw(), + section.into().as_ptr(), + key.into().as_ptr(), + value.into().as_ptr(), + ) != 0 + } + + /// Get the extended state value for a specific section and key. + pub fn get_project_ext_state<'a>( + &self, + project: ProjectContext, + section: impl Into>, + key: impl Into>, + buffer_size: u32, + ) -> ReaperFunctionResult + where + UsageScope: MainThreadOnly, + { + self.require_valid_project(project); + self.get_project_ext_state_unchecked(project, section, key, buffer_size) + } + + pub fn get_project_ext_state_unchecked<'a>( + &self, + project: ProjectContext, + section: impl Into>, + key: impl Into>, + buffer_size: u32, + ) -> ReaperFunctionResult + where + UsageScope: MainThreadOnly, + { + unsafe { + let (value, result) = with_string_buffer(buffer_size, |buf, size| { + self.low().GetProjExtState( + project.to_raw(), + section.into().as_ptr(), + key.into().as_ptr(), + buf, + size, + ) + }); + match result { + x if x < 0 => Err(ReaperFunctionError::new("Can not get ext state.")), + _ => Ok(value), + } + } + } + + /// Delete the extended state value for a specific section and key. + /// + /// if key = None, deletes the whole section. + /// + /// # Panics + /// + /// If you pass an invalid project. + pub fn delete_project_ext_state<'a>( + &self, + project: ProjectContext, + section: impl Into>, + key: Option>>, + ) where + UsageScope: MainThreadOnly, + { + self.require_valid_project(project); + self.delete_project_ext_state_unchecked(project, section, key) + } + + pub fn delete_project_ext_state_unchecked<'a>( + &self, + project: ProjectContext, + section: impl Into>, + key: Option>>, + ) where + UsageScope: MainThreadOnly, + { + let key = match key { + None => ReaperStringArg::from(""), + Some(val) => val.into(), + }; + unsafe { + self.low().SetProjExtState( + project.to_raw(), + section.into().as_ptr(), + key.as_ptr(), + ReaperStringArg::from("").as_ptr(), + ); + } + } + + /// Enumerate the data stored with the project for a specific extname. + /// + /// result.is_more is false, when there is no more data. + /// + /// index is 0-based. + pub fn enum_project_ext_state<'a>( + &self, + project: ProjectContext, + section: impl Into>, + index: u32, + key_size: u32, + val_size: u32, + ) -> EnumProjectExtStateResult + where + UsageScope: MainThreadOnly, + { + self.require_valid_project(project); + unsafe { + self.enum_project_ext_state_unchecked(project, section, index, key_size, val_size) + } + } + + pub unsafe fn enum_project_ext_state_unchecked<'a>( + &self, + project: ProjectContext, + section: impl Into>, + index: u32, + key_size: u32, + val_size: u32, + ) -> EnumProjectExtStateResult + where + UsageScope: MainThreadOnly, + { + let key_buf = create_string_buffer(key_size); + let val_buf = create_string_buffer(val_size); + let state = self.low().EnumProjExtState( + project.to_raw(), + section.into().as_ptr(), + index as i32, + key_buf, + key_size as i32, + val_buf, + val_size as i32, + ); + EnumProjectExtStateResult { + is_empty: !state, + key: ReaperStr::from_ptr(key_buf).to_reaper_string(), + value: ReaperStr::from_ptr(val_buf).to_reaper_string(), + } + } + /// Sets the volume of the given track send, hardware output send or track receive. /// /// # Errors @@ -7301,6 +7548,23 @@ pub enum GetFocusedFxResult { Unknown(Hidden), } +#[derive(Clone, PartialEq, Hash, Debug)] +pub struct EnumProjectExtStateResult { + /// true, if function can be called with the next index. + pub is_empty: bool, + pub key: ReaperString, + pub value: ReaperString, +} +impl EnumProjectExtStateResult { + pub fn new(is_empty: bool, key: &str, value: &str) -> Self { + Self { + is_empty, + key: ReaperString::from_str(key), + value: ReaperString::from_str(value), + } + } +} + #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct RgbColor { pub r: u8, diff --git a/main/medium/src/util.rs b/main/medium/src/util.rs index 89300366..79c98068 100644 --- a/main/medium/src/util.rs +++ b/main/medium/src/util.rs @@ -47,16 +47,28 @@ pub fn with_string_buffer_internal( (string, result) } +pub fn create_string_buffer(max_size: u32) -> *mut i8 { + let vec: Vec = vec![0; max_size as usize]; + let c_string = unsafe { CString::from_vec_unchecked(vec) }; + let raw = c_string.into_raw(); + raw +} + pub fn with_buffer( max_size: u32, fill_buffer: impl FnOnce(*mut c_char, i32) -> T, ) -> (Vec, T) { - let mut vec: Vec = vec![0; max_size as usize]; - let raw = vec.as_mut_ptr() as *mut c_char; + let (vec, raw) = create_buffer(max_size); let result = fill_buffer(raw, max_size as i32); (vec, result) } +pub fn create_buffer(max_size: u32) -> (Vec, *mut i8) { + let mut vec: Vec = vec![0; max_size as usize]; + let raw = vec.as_mut_ptr() as *mut c_char; + (vec, raw) +} + /// We really need a box here in order to obtain a thin pointer. We must not consume it, that's why /// we take it as reference. #[allow(clippy::borrowed_box)] diff --git a/test/test/src/tests.rs b/test/test/src/tests.rs index 7523966c..e154df76 100644 --- a/test/test/src/tests.rs +++ b/test/test/src/tests.rs @@ -23,11 +23,12 @@ use helgoboss_midi::{RawShortMessage, ShortMessageFactory}; use reaper_medium::ProjectContext::CurrentProject; use reaper_medium::{ reaper_str, AutoSeekBehavior, AutomationMode, Bpm, CommandId, Db, DurationInSeconds, EditMode, - EnumPitchShiftModesResult, FxPresetRef, GangBehavior, GetParamExResult, InputMonitoringMode, - MasterTrackBehavior, MidiInputDeviceId, MidiOutputDeviceId, NormalizedPlayRate, PitchShiftMode, - PlaybackSpeedFactor, PositionInSeconds, ReaperNormalizedFxParamValue, ReaperPanValue, - ReaperVersion, ReaperVolumeValue, ReaperWidthValue, RecordingInput, SoloMode, - StuffMidiMessageTarget, TrackFxGetPresetIndexResult, TrackLocation, UndoBehavior, ValueChange, + EnumPitchShiftModesResult, EnumProjectExtStateResult, FxPresetRef, GangBehavior, + GetParamExResult, InputMonitoringMode, MasterTrackBehavior, MidiInputDeviceId, + MidiOutputDeviceId, NormalizedPlayRate, PitchShiftMode, PlaybackSpeedFactor, PositionInSeconds, + ReaperNormalizedFxParamValue, ReaperPanValue, ReaperVersion, ReaperVolumeValue, + ReaperWidthValue, RecordingInput, SoloMode, StuffMidiMessageTarget, + TrackFxGetPresetIndexResult, TrackLocation, UndoBehavior, ValueChange, }; use reaper_low::{raw, Swell}; @@ -122,6 +123,8 @@ pub fn create_test_steps() -> impl Iterator { main_section_functions(), register_and_unregister_action(), register_and_unregister_toggle_action(), + project_info_string(), + ext_state(), ] .into_iter(); let steps_b = vec![ @@ -528,6 +531,63 @@ fn register_and_unregister_action() -> TestStep { ) } +fn ext_state() -> TestStep { + step(AllVersions, "Ext State", |rpr, _| { + let medium = rpr.medium_reaper(); + let (section, key, value) = ("MySection", "NewKey", "SomeValue"); + let result = medium.get_ext_state(section, key); + assert_eq!(&result.to_string(), ""); + medium.set_ext_state(section, String::from(key), value, false); + let result = medium.get_ext_state(section, key); + assert_eq!(&result.to_string(), value); + + // Project + + let pr = rpr.current_project(); + let (key2, value2) = ("Another Key", "Strange valu€"); + let (key_buf_size, val_buf_size) = (50, 50); + assert_eq!( + &medium + .get_project_ext_state(pr.context(), section, key, val_buf_size)? + .to_string(), + "" + ); + let result = + medium.enum_project_ext_state(pr.context(), section, 0, key_buf_size, val_buf_size); + assert_eq!(result.is_empty, true); + assert_eq!(&result.key.into_string(), ""); + assert_eq!(&result.value.into_string(), ""); + + // set + + medium.set_project_ext_state(pr.context(), section, key, value); + medium.set_project_ext_state(pr.context(), section, key2, value2); + let result = + medium.enum_project_ext_state(pr.context(), section, 0, key_buf_size, val_buf_size); + assert_eq!( + result, + EnumProjectExtStateResult::new(false, &key2.to_uppercase(), value2) + ); + let result = + medium.enum_project_ext_state(pr.context(), section, 1, key_buf_size, val_buf_size); + assert_eq!( + result, + EnumProjectExtStateResult::new(false, &key.to_uppercase(), value) + ); + let result = + medium.enum_project_ext_state(pr.context(), section, 2, key_buf_size, val_buf_size); + assert_eq!(result, EnumProjectExtStateResult::new(true, "", "")); + + assert_eq!( + medium + .get_project_ext_state(pr.context(), section, key2, val_buf_size)? + .to_str(), + value2 + ); + Ok(()) + }) +} + fn main_section_functions() -> TestStep { step(AllVersions, "Main section functions", |_reaper, _| { // Given