Skip to content

Commit

Permalink
#394 Add possibility to provide group and mapping tags (searchable)
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Sep 7, 2021
1 parent fbbf400 commit 307aaba
Show file tree
Hide file tree
Showing 22 changed files with 306 additions and 79 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

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

4 changes: 3 additions & 1 deletion main/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "realearn"
version = "2.10.0"
version = "2.11.0"
authors = ["Benjamin Klum <benjamin.klum@helgoboss.org>"]
edition = "2018"
build = "build.rs"
Expand Down Expand Up @@ -29,6 +29,8 @@ rx-util = { path = "../rx-util" }
helgoboss-midi = { version = "0.2", features = ["serde", "serde_repr"] }
# In future (when helgoboss-learn has matured), this will become a crates.io dependency
helgoboss-learn = { path = "lib/helgoboss-learn", features = ["serde", "serde_repr", "serde_with", "reaper-low"] }
# For being able to (de)serialize using FromStr and Display
serde_with = "1.6.4"
c_str_macro = "1.0.2"
vst = "0.2.1"
rxrust = { git = "https://github.com/rxRust/rxRust", branch = "master" }
Expand Down
5 changes: 4 additions & 1 deletion main/src/application/group_model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::application::{ActivationConditionModel, GroupData};
use crate::base::{prop, Prop};
use crate::domain::{GroupId, MappingCompartment};
use crate::domain::{GroupId, MappingCompartment, Tag};
use core::fmt;
use rxrust::prelude::*;
use std::cell::RefCell;
Expand All @@ -12,6 +12,7 @@ pub struct GroupModel {
compartment: MappingCompartment,
id: GroupId,
pub name: Prop<String>,
pub tags: Prop<Vec<Tag>>,
pub control_is_enabled: Prop<bool>,
pub feedback_is_enabled: Prop<bool>,
pub activation_condition_model: ActivationConditionModel,
Expand Down Expand Up @@ -51,6 +52,7 @@ impl GroupModel {
compartment,
id: Default::default(),
name: Default::default(),
tags: Default::default(),
control_is_enabled: prop(true),
feedback_is_enabled: prop(true),
activation_condition_model: ActivationConditionModel::default(),
Expand Down Expand Up @@ -84,6 +86,7 @@ impl GroupModel {
activation_condition: self
.activation_condition_model
.create_activation_condition(),
tags: self.tags.get_ref().clone(),
}
}

Expand Down
9 changes: 8 additions & 1 deletion main/src/application/mapping_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::domain::{
ActivationCondition, CompoundMappingSource, CompoundMappingTarget, ExtendedProcessorContext,
ExtendedSourceCharacter, FeedbackSendBehavior, GroupId, MainMapping, MappingCompartment,
MappingId, Mode, ProcessorMappingOptions, QualifiedMappingId, RealearnTarget, ReaperTarget,
TargetCharacter, UnresolvedCompoundMappingTarget,
Tag, TargetCharacter, UnresolvedCompoundMappingTarget,
};
use helgoboss_learn::{
AbsoluteMode, ControlType, DetailedSourceCharacter, Interval, ModeApplicabilityCheckInput,
Expand All @@ -24,6 +24,7 @@ pub struct MappingModel {
id: MappingId,
compartment: MappingCompartment,
pub name: Prop<String>,
pub tags: Prop<Vec<Tag>>,
pub group_id: Prop<GroupId>,
pub control_is_enabled: Prop<bool>,
pub feedback_is_enabled: Prop<bool>,
Expand Down Expand Up @@ -72,6 +73,7 @@ impl MappingModel {
id: MappingId::random(),
compartment,
name: Default::default(),
tags: Default::default(),
group_id: prop(initial_group_id),
control_is_enabled: prop(true),
feedback_is_enabled: prop(true),
Expand Down Expand Up @@ -296,11 +298,14 @@ impl MappingModel {
feedback_is_enabled: group_data.feedback_is_enabled && self.feedback_is_enabled.get(),
feedback_send_behavior: self.feedback_send_behavior.get(),
};
let mut merged_tags = group_data.tags;
merged_tags.extend_from_slice(self.tags.get_ref());
MainMapping::new(
self.compartment,
id,
self.group_id.get(),
self.name.get_ref().clone(),
merged_tags,
source,
mode,
self.mode_model.group_interaction.get(),
Expand All @@ -319,6 +324,7 @@ pub struct GroupData {
pub control_is_enabled: bool,
pub feedback_is_enabled: bool,
pub activation_condition: ActivationCondition,
pub tags: Vec<Tag>,
}

impl Default for GroupData {
Expand All @@ -327,6 +333,7 @@ impl Default for GroupData {
control_is_enabled: true,
feedback_is_enabled: true,
activation_condition: ActivationCondition::Always,
tags: vec![],
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions main/src/application/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,18 @@ impl Session {
.find(|g| g.borrow().id() == id)
}

pub fn find_group_by_id_including_default_group(
&self,
compartment: MappingCompartment,
id: GroupId,
) -> Option<&SharedGroup> {
if id.is_default() {
Some(self.default_group(compartment))
} else {
self.find_group_by_id(compartment, id)
}
}

pub fn find_group_by_index_sorted(
&self,
compartment: MappingCompartment,
Expand Down
9 changes: 6 additions & 3 deletions main/src/domain/main_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::domain::ui_util::{
format_short_midi_message, log_control_input, log_feedback_output, log_learn_input,
log_lifecycle_output, log_target_output,
};
use ascii::{AsciiString, ToAsciiChar};
use helgoboss_midi::RawShortMessage;
use reaper_high::{ChangeEvent, Reaper};
use reaper_medium::ReaperNormalizedFxParamValue;
Expand Down Expand Up @@ -2507,9 +2508,11 @@ impl fmt::Display for InstanceId {
impl InstanceId {
pub fn random() -> Self {
let instance_id = nanoid::nanoid!(8);
let ascii = SmallAsciiString::create_compatible_ascii_string(&instance_id);
let small_ascii = SmallAsciiString::from_ascii_str(&ascii).expect("impossible");
Self(small_ascii)
let ascii_string: AsciiString = instance_id
.chars()
.filter_map(|c| c.to_ascii_char().ok())
.collect();
Self(SmallAsciiString::from_ascii_str_cropping(&ascii_string))
}
}

Expand Down
8 changes: 6 additions & 2 deletions main/src/domain/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use crate::domain::{
ExtendedProcessorContext, FeedbackResolution, GroupId, HitInstructionReturnValue,
InstanceFeedbackEvent, MappingActivationEffect, MidiSource, Mode, ParameterArray,
ParameterSlice, RealSource, RealTimeReaperTarget, RealearnTarget, ReaperMessage, ReaperSource,
ReaperTarget, TargetCharacter, TrackExclusivity, UnresolvedReaperTarget, VirtualControlElement,
VirtualSource, VirtualSourceValue, VirtualTarget, COMPARTMENT_PARAMETER_COUNT,
ReaperTarget, Tag, TargetCharacter, TrackExclusivity, UnresolvedReaperTarget,
VirtualControlElement, VirtualSource, VirtualSourceValue, VirtualTarget,
COMPARTMENT_PARAMETER_COUNT,
};
use derive_more::Display;
use enum_iterator::IntoEnumIterator;
Expand Down Expand Up @@ -126,6 +127,7 @@ impl MappingExtension {
pub struct MainMapping {
core: MappingCore,
name: String,
tags: Vec<Tag>,
/// Is `Some` if the user-provided target data is complete.
unresolved_target: Option<UnresolvedCompoundMappingTarget>,
/// Is non-empty if the target resolved successfully.
Expand Down Expand Up @@ -156,6 +158,7 @@ impl MainMapping {
id: MappingId,
group_id: GroupId,
name: String,
tags: Vec<Tag>,
source: CompoundMappingSource,
mode: Mode,
group_interaction: GroupInteraction,
Expand All @@ -177,6 +180,7 @@ impl MainMapping {
time_of_last_control: None,
},
name,
tags,
unresolved_target,
targets: vec![],
activation_condition_1,
Expand Down
6 changes: 6 additions & 0 deletions main/src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,9 @@ pub use reaper_source::*;

mod device_change_detector;
pub use device_change_detector::*;

mod small_ascii_string;
pub use small_ascii_string::*;

mod tag;
pub use tag::*;
52 changes: 52 additions & 0 deletions main/src/domain/small_ascii_string.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use ascii::{AsciiStr, AsciiString};
use core::fmt;

/// String with a maximum of 16 ASCII characters.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub struct SmallAsciiString {
length: u8,
content: [u8; SmallAsciiString::MAX_LENGTH],
}

impl SmallAsciiString {
pub const MAX_LENGTH: usize = 16;

/// Crops the string if necessary.
pub fn from_ascii_str_cropping(ascii_str: &AsciiStr) -> Self {
let short =
AsciiString::from(&ascii_str.as_slice()[..Self::MAX_LENGTH.min(ascii_str.len())]);
Self::from_ascii_str(&short)
}

/// Returns an error if the given string is too long.
pub fn try_from_ascii_str(ascii_str: &AsciiStr) -> Result<Self, &'static str> {
if ascii_str.len() > SmallAsciiString::MAX_LENGTH {
return Err("too large to be a small ASCII string");
}
Ok(Self::from_ascii_str(ascii_str))
}

/// Panics if the given string is too long.
fn from_ascii_str(ascii_str: &AsciiStr) -> Self {
let mut content = [0u8; SmallAsciiString::MAX_LENGTH];
content[..ascii_str.len()].copy_from_slice(ascii_str.as_bytes());
Self {
content,
length: ascii_str.len() as u8,
}
}

pub fn as_ascii_str(&self) -> &AsciiStr {
AsciiStr::from_ascii(self.as_slice()).unwrap()
}

pub fn as_slice(&self) -> &[u8] {
&self.content[..(self.length as usize)]
}
}

impl fmt::Display for SmallAsciiString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_ascii_str().fmt(f)
}
}
56 changes: 56 additions & 0 deletions main/src/domain/tag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::domain::SmallAsciiString;
use ascii::{AsciiChar, AsciiString, ToAsciiChar};
use core::fmt;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::fmt::{Display, Formatter};
use std::str::FromStr;

/// We reduce the number of possible letters in case we want to use tags in the audio thread in
/// future (and therefore need to avoid allocation).
#[derive(
Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash, SerializeDisplay, DeserializeFromStr,
)]
pub struct Tag(SmallAsciiString);

impl FromStr for Tag {
type Err = &'static str;

fn from_str(text: &str) -> Result<Self, Self::Err> {
let ascii_string: AsciiString = text
.chars()
// Remove all non-ASCII schars
.filter_map(|c| c.to_ascii_char().ok())
// Allow only letters, digits and underscore
.filter(|c| c.is_ascii_alphanumeric() || *c == AsciiChar::UnderScore)
// Skip leading digits
.skip_while(|c| c.is_ascii_digit())
.collect();
if ascii_string.is_empty() {
return Err("empty tag");
}
let small_ascii_string = SmallAsciiString::from_ascii_str_cropping(&ascii_string);
Ok(Self(small_ascii_string))
}
}

impl Display for Tag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
pub fn parse_tags() {
assert_eq!(Tag::from_str("hey").unwrap().to_string(), "hey");
assert_eq!(Tag::from_str("hey_test").unwrap().to_string(), "hey_test");
assert_eq!(
Tag::from_str("1ähey1ätest").unwrap().to_string(),
"hey1test"
);
assert!(Tag::from_str("1ä").is_err());
}
}
Loading

0 comments on commit 307aaba

Please sign in to comment.