Skip to content

Commit

Permalink
#422 ReaLearn: Support MIDI editor and media explorer actions
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Aug 24, 2024
1 parent 8d54969 commit cc9dd1b
Show file tree
Hide file tree
Showing 19 changed files with 256 additions and 105 deletions.
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 51 additions & 1 deletion api/src/persistence/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use strum::IntoEnumIterator;
use strum::{EnumIter, IntoEnumIterator};

#[derive(
Copy,
Expand Down Expand Up @@ -229,13 +229,63 @@ pub struct ReaperActionTarget {
#[serde(flatten)]
pub commons: TargetCommons,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<ActionScope>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<ReaperCommand>,
#[serde(skip_serializing_if = "Option::is_none")]
pub invocation: Option<ActionInvocationKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub track: Option<TrackDescriptor>,
}

#[derive(
Copy,
Clone,
Eq,
PartialEq,
Hash,
Debug,
Default,
EnumIter,
TryFromPrimitive,
IntoPrimitive,
Display,
Serialize,
Deserialize,
)]
#[repr(usize)]
pub enum ActionScope {
#[display(fmt = "Main")]
#[default]
Main,
#[display(fmt = "Active MIDI editor")]
ActiveMidiEditor,
#[display(fmt = "Active MIDI event list editor")]
ActiveMidiEventListEditor,
#[display(fmt = "Media explorer")]
MediaExplorer,
}

impl ActionScope {
pub fn guess_from_section_id(section_id: u32) -> Self {
match section_id {
32060 => ActionScope::ActiveMidiEditor,
32061 => ActionScope::ActiveMidiEventListEditor,
32063 => ActionScope::MediaExplorer,
_ => ActionScope::Main,
}
}

pub fn section_id(&self) -> u32 {
match self {
ActionScope::Main => 0,
ActionScope::ActiveMidiEditor => 32060,
ActionScope::ActiveMidiEventListEditor => 32061,
ActionScope::MediaExplorer => 32063,
}
}
}

#[derive(Eq, PartialEq, Serialize, Deserialize)]
pub struct TransportActionTarget {
#[serde(flatten)]
Expand Down
2 changes: 1 addition & 1 deletion main/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "helgobox"
version = "2.16.2"
version = "2.16.3"
authors = ["Benjamin Klum <[email protected]>"]
edition = "2021"
build = "build.rs"
Expand Down
35 changes: 35 additions & 0 deletions main/src/application/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use crate::base::notification;
use reaper_high::{Action, Reaper};
use reaper_medium::SectionId;

pub fn build_smart_command_name_from_action(action: &Action) -> Option<String> {
match action.command_name() {
// Built-in actions don't have a command name but a persistent command ID.
// Use command ID as string.
None => action.command_id().ok().map(|id| id.to_string()),
// ReaScripts and custom actions have a command name as persistent identifier.
Some(name) => Some(name.into_string()),
}
}

pub fn build_action_from_smart_command_name(
section_id: SectionId,
smart_command_name: &str,
) -> Option<Action> {
match smart_command_name.parse::<u32>() {
// Could parse this as command ID integer. This is a built-in action.
Ok(command_id_int) => match command_id_int.try_into() {
Ok(command_id) => Some(
Reaper::get()
.section_by_id(section_id)
.action_by_command_id(command_id),
),
Err(_) => {
notification::warn(format!("Invalid command ID {command_id_int}"));
None
}
},
// Couldn't parse this as integer. This is a ReaScript or custom action.
Err(_) => Some(Reaper::get().action_by_command_name(smart_command_name)),
}
}
4 changes: 4 additions & 0 deletions main/src/application/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ mod props;
pub use props::*;

mod auto_units;

mod actions;
pub use actions::*;

pub use auto_units::*;
71 changes: 47 additions & 24 deletions main/src/application/target_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use reaper_high::{
use serde::{Deserialize, Serialize};

use crate::application::{
Affected, Change, GetProcessingRelevance, ProcessingRelevance, UnitModel,
build_action_from_smart_command_name, build_smart_command_name_from_action, Affected, Change,
GetProcessingRelevance, ProcessingRelevance, UnitModel,
};
use crate::domain::{
find_bookmark, get_fx_name, get_fx_params, get_non_present_virtual_route_label,
Expand Down Expand Up @@ -59,19 +60,19 @@ use std::error::Error;
use crate::domain::ui_util::format_tags_as_csv;
use base::hash_util::NonCryptoHashSet;
use helgobox_api::persistence::{
Axis, BrowseTracksMode, ClipColumnTrackContext, FxChainDescriptor, FxDescriptorCommons,
FxToolAction, LearnTargetMappingModification, LearnableTargetKind, MappingModification,
MappingSnapshotDescForLoad, MappingSnapshotDescForTake, MonitoringMode, MouseAction,
MouseButton, PlaytimeColumnAction, PlaytimeColumnDescriptor, PlaytimeMatrixAction,
ActionScope, Axis, BrowseTracksMode, ClipColumnTrackContext, FxChainDescriptor,
FxDescriptorCommons, FxToolAction, LearnTargetMappingModification, LearnableTargetKind,
MappingModification, MappingSnapshotDescForLoad, MappingSnapshotDescForTake, MonitoringMode,
MouseAction, MouseButton, PlaytimeColumnAction, PlaytimeColumnDescriptor, PlaytimeMatrixAction,
PlaytimeRowAction, PlaytimeRowDescriptor, PlaytimeSlotDescriptor, PlaytimeSlotManagementAction,
PlaytimeSlotTransportAction, PotFilterKind, SeekBehavior,
SetTargetToLastTouchedMappingModification, TargetTouchCause, TrackDescriptorCommons,
TrackFxChain, TrackScope, TrackToolAction, VirtualControlElementCharacter,
};
use playtime_api::persistence::ColumnAddress;
use reaper_medium::{
AutomationMode, BookmarkId, GlobalAutomationModeOverride, InputMonitoringMode, TrackArea,
TrackLocation, TrackSendDirection,
AutomationMode, BookmarkId, GlobalAutomationModeOverride, InputMonitoringMode, SectionId,
TrackArea, TrackLocation, TrackSendDirection,
};
use std::fmt;
use std::fmt::{Display, Formatter};
Expand All @@ -87,7 +88,8 @@ pub enum TargetCommand {
SetControlElementId(VirtualControlElementId),
SetLearnable(bool),
SetTargetType(ReaperTargetType),
SetAction(Option<Action>),
SetActionScope(ActionScope),
SetSmartCommandName(Option<String>),
SetActionInvocationType(ActionInvocationType),
SetWithTrack(bool),
SetTrackName(String),
Expand Down Expand Up @@ -182,7 +184,8 @@ pub enum TargetProp {
ControlElementId,
Learnable,
TargetType,
Action,
ActionScope,
SmartCommandName,
ActionInvocationType,
WithTrack,
TrackType,
Expand Down Expand Up @@ -313,9 +316,13 @@ impl<'a> Change<'a> for TargetModel {
self.r#type = v;
One(P::TargetType)
}
C::SetAction(v) => {
self.action = v;
One(P::Action)
C::SetActionScope(v) => {
self.action_scope = v;
One(P::ActionScope)
}
C::SetSmartCommandName(v) => {
self.smart_command_name = v;
One(P::SmartCommandName)
}
C::SetActionInvocationType(v) => {
self.action_invocation_type = v;
Expand Down Expand Up @@ -672,8 +679,8 @@ pub struct TargetModel {
// TODO-low Rename this to reaper_target_type
r#type: ReaperTargetType,
// # For action targets only
// TODO-low Maybe replace Action with just command ID and/or command name
action: Option<Action>,
action_scope: ActionScope,
smart_command_name: Option<String>,
action_invocation_type: ActionInvocationType,
with_track: bool,
// # For track targets
Expand Down Expand Up @@ -847,7 +854,8 @@ impl Default for TargetModel {
control_element_id: Default::default(),
learnable: true,
r#type: ReaperTargetType::Dummy,
action: None,
action_scope: Default::default(),
smart_command_name: None,
action_invocation_type: ActionInvocationType::default(),
track_type: Default::default(),
track_id: None,
Expand Down Expand Up @@ -966,8 +974,12 @@ impl TargetModel {
self.r#type
}

pub fn action(&self) -> Option<&Action> {
self.action.as_ref()
pub fn action_scope(&self) -> ActionScope {
self.action_scope
}

pub fn smart_command_name(&self) -> Option<&str> {
self.smart_command_name.as_deref()
}

pub fn action_invocation_type(&self) -> ActionInvocationType {
Expand Down Expand Up @@ -1730,7 +1742,9 @@ impl TargetModel {

match target {
Action(t) => {
self.action = Some(t.action.clone());
let section_id = t.action.section().map(|s| s.id()).unwrap_or_default();
self.action_scope = ActionScope::guess_from_section_id(section_id.get());
self.smart_command_name = build_smart_command_name_from_action(&t.action);
self.action_invocation_type = t.invocation_type;
}
FxParameter(t) => {
Expand Down Expand Up @@ -2243,7 +2257,8 @@ impl TargetModel {
},
),
Action => UnresolvedReaperTarget::Action(UnresolvedActionTarget {
action: self.resolved_action()?,
action: self.resolved_available_action()?,
scope: self.action_scope,
invocation_type: self.action_invocation_type,
track_descriptor: if self.with_track {
Some(self.track_descriptor()?)
Expand Down Expand Up @@ -2962,7 +2977,7 @@ impl TargetModel {
}

fn command_id_label(&self) -> Cow<str> {
match self.action.as_ref() {
match self.resolve_action() {
None => "-".into(),
Some(action) => {
if action.is_available() {
Expand All @@ -2980,16 +2995,24 @@ impl TargetModel {
}
}

pub fn resolved_action(&self) -> Result<Action, &'static str> {
let action = self.action.as_ref().ok_or("action not set")?;
pub fn resolve_action(&self) -> Option<Action> {
let command_name = self.smart_command_name.as_deref()?;
build_action_from_smart_command_name(
SectionId::new(self.action_scope.section_id()),
command_name,
)
}

pub fn resolved_available_action(&self) -> Result<Action, &'static str> {
let action = self.resolve_action().ok_or("action not set")?;
if !action.is_available() {
return Err("action not available");
}
Ok(action.clone())
}

pub fn action_name_label(&self) -> Cow<str> {
match self.resolved_action().ok() {
match self.resolved_available_action().ok() {
None => "-".into(),
Some(a) => a.name().expect("should be available").into_string().into(),
}
Expand All @@ -3006,7 +3029,7 @@ impl<'a> Display for TargetModelFormatVeryShort<'a> {
use ReaperTargetType::*;
let tt = self.0.r#type;
match tt {
Action => match self.0.resolved_action().ok() {
Action => match self.0.resolved_available_action().ok() {
None => write!(f, "Action {}", self.0.command_id_label()),
Some(a) => f.write_str(a.name().expect("should be available").to_str()),
},
Expand Down
2 changes: 1 addition & 1 deletion main/src/domain/realearn_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub trait RealearnTarget {
Reaper::get()
.main_section()
.action_by_command_id(CommandId::new(40913))
.invoke_as_trigger(Some(track.project()))
.invoke_as_trigger(Some(track.project()), None)
.expect("built-in action should exist");
}
}
Expand Down
Loading

0 comments on commit cc9dd1b

Please sign in to comment.