maolan 0.2.5

Rust DAW application for recording, editing, routing, automation, export, and plugin hosting
use crate::message::{Message, Show};
use maolan_widgets::iced::keyboard::{self, Modifiers};
use std::collections::HashMap;
use std::fmt;

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, PartialOrd, Ord,
)]
#[serde(rename_all = "snake_case")]
pub enum ShortcutAction {
    NewSession,
    OpenSession,
    SaveSession,
    SaveSessionAs,
    ImportFiles,
    Export,
    AddTrack,
    SelectAll,
    RecordArmToggle,
    MidiPanic,
    Undo,
    Redo,
    RedoAlternate,
    RemoveSelected,
    RemoveSelectedBackspace,
    Escape,
    ToggleShortcutsPane,
    ToggleModulatorsPane,
    ToggleClipsPane,
    ToggleCutIndicator,
    ToggleSelectedPluginBypass,
    ToggleWorkspaceSession,
    QuantizeSelectedNotes,
    HumanizeSelectedNotes,
    GrooveSelectedNotes,
    ToggleTransport,
    PauseTransport,
    JumpToStart,
    JumpToEnd,
    SessionNavUp,
    SessionNavDown,
    SessionNavLeft,
    SessionNavRight,
    SessionNavLaunch,
    SessionStopAll,
}

impl ShortcutAction {
    pub const ALL: [Self; 35] = [
        Self::NewSession,
        Self::OpenSession,
        Self::SaveSession,
        Self::SaveSessionAs,
        Self::ImportFiles,
        Self::Export,
        Self::AddTrack,
        Self::SelectAll,
        Self::RecordArmToggle,
        Self::MidiPanic,
        Self::Undo,
        Self::Redo,
        Self::RedoAlternate,
        Self::RemoveSelected,
        Self::RemoveSelectedBackspace,
        Self::Escape,
        Self::ToggleShortcutsPane,
        Self::ToggleModulatorsPane,
        Self::ToggleClipsPane,
        Self::ToggleCutIndicator,
        Self::ToggleSelectedPluginBypass,
        Self::ToggleWorkspaceSession,
        Self::QuantizeSelectedNotes,
        Self::HumanizeSelectedNotes,
        Self::GrooveSelectedNotes,
        Self::ToggleTransport,
        Self::PauseTransport,
        Self::JumpToStart,
        Self::JumpToEnd,
        Self::SessionNavUp,
        Self::SessionNavDown,
        Self::SessionNavLeft,
        Self::SessionNavRight,
        Self::SessionNavLaunch,
        Self::SessionStopAll,
    ];

    pub const fn message(self, current_view: crate::state::View) -> Message {
        match self {
            Self::NewSession => Message::NewSession,
            Self::OpenSession => Message::Show(Show::Open),
            Self::SaveSession => Message::Show(Show::Save),
            Self::SaveSessionAs => Message::Show(Show::SaveAs),
            Self::ImportFiles => Message::OpenFileImporter,
            Self::Export => Message::OpenExporter,
            Self::AddTrack => Message::Show(Show::AddTrack),
            Self::SelectAll => Message::SelectAll,
            Self::RecordArmToggle => Message::TransportRecordToggle,
            Self::MidiPanic => Message::TransportPanic,
            Self::Undo => Message::Undo,
            Self::Redo | Self::RedoAlternate => Message::Redo,
            Self::RemoveSelected | Self::RemoveSelectedBackspace => Message::Remove,
            Self::Escape => Message::EscapePressed,
            Self::ToggleShortcutsPane => Message::ToggleShortcutsPane,
            Self::ToggleModulatorsPane => Message::ToggleModulatorsPane,
            Self::ToggleClipsPane => Message::ToggleClipsPane,
            Self::ToggleCutIndicator => Message::ToggleCutIndicator,
            Self::ToggleSelectedPluginBypass => Message::ToggleSelectedPluginBypass,
            Self::ToggleWorkspaceSession => {
                if matches!(current_view, crate::state::View::Session) {
                    Message::Workspace
                } else {
                    Message::Session
                }
            }
            Self::QuantizeSelectedNotes => Message::PianoQuantizeSelectedNotes,
            Self::HumanizeSelectedNotes => Message::PianoHumanizeSelectedNotes,
            Self::GrooveSelectedNotes => Message::PianoGrooveSelectedNotes,
            Self::ToggleTransport => Message::ToggleTransport,
            Self::PauseTransport => Message::TransportPause,
            Self::JumpToStart => Message::JumpToStart,
            Self::JumpToEnd => Message::JumpToEnd,
            Self::SessionNavUp => Message::SessionNavMove {
                delta_x: 0,
                delta_y: -1,
            },
            Self::SessionNavDown => Message::SessionNavMove {
                delta_x: 0,
                delta_y: 1,
            },
            Self::SessionNavLeft => Message::SessionNavMove {
                delta_x: -1,
                delta_y: 0,
            },
            Self::SessionNavRight => Message::SessionNavMove {
                delta_x: 1,
                delta_y: 0,
            },
            Self::SessionNavLaunch => Message::SessionNavLaunch,
            Self::SessionStopAll => Message::SessionNavStopAll,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ShortcutBinding {
    pub key: ShortcutKey,
    #[serde(default)]
    pub ctrl: bool,
    #[serde(default)]
    pub shift: bool,
}

impl ShortcutBinding {
    pub const fn new(key: ShortcutKey, ctrl: bool, shift: bool) -> Self {
        Self { key, ctrl, shift }
    }

    pub fn from_iced_key(key: &keyboard::Key, modifiers: Modifiers) -> Option<Self> {
        let key = ShortcutKey::from_iced_key(key)?;
        Some(Self {
            key,
            ctrl: modifiers.control(),
            shift: modifiers.shift(),
        })
    }

    pub fn label(&self) -> String {
        let mut parts = Vec::new();
        if self.ctrl {
            parts.push("Ctrl".to_string());
        }
        if self.shift {
            parts.push("Shift".to_string());
        }
        parts.push(self.key.to_string());
        parts.join("+")
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShortcutKey {
    Character(String),
    Space,
    Tab,
    Return,
    Home,
    End,
    ArrowUp,
    ArrowDown,
    ArrowLeft,
    ArrowRight,
    Delete,
    Backspace,
    Escape,
}

impl ShortcutKey {
    fn from_iced_key(key: &keyboard::Key) -> Option<Self> {
        match key {
            keyboard::Key::Character(ch) => {
                let normalized = ch.trim().to_ascii_lowercase();
                (!normalized.is_empty()).then_some(Self::Character(normalized))
            }
            keyboard::Key::Named(keyboard::key::Named::Space) => Some(Self::Space),
            keyboard::Key::Named(keyboard::key::Named::Tab) => Some(Self::Tab),
            keyboard::Key::Named(keyboard::key::Named::Enter) => Some(Self::Return),
            keyboard::Key::Named(keyboard::key::Named::Home) => Some(Self::Home),
            keyboard::Key::Named(keyboard::key::Named::End) => Some(Self::End),
            keyboard::Key::Named(keyboard::key::Named::ArrowUp) => Some(Self::ArrowUp),
            keyboard::Key::Named(keyboard::key::Named::ArrowDown) => Some(Self::ArrowDown),
            keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => Some(Self::ArrowLeft),
            keyboard::Key::Named(keyboard::key::Named::ArrowRight) => Some(Self::ArrowRight),
            keyboard::Key::Named(keyboard::key::Named::Delete) => Some(Self::Delete),
            keyboard::Key::Named(keyboard::key::Named::Backspace) => Some(Self::Backspace),
            keyboard::Key::Named(keyboard::key::Named::Escape) => Some(Self::Escape),
            _ => None,
        }
    }
}

impl fmt::Display for ShortcutKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Character(ch) => write!(f, "{}", ch.to_ascii_uppercase()),
            Self::Space => write!(f, "Space"),
            Self::Tab => write!(f, "Tab"),
            Self::Return => write!(f, "Return"),
            Self::Home => write!(f, "Home"),
            Self::End => write!(f, "End"),
            Self::ArrowUp => write!(f, "ArrowUp"),
            Self::ArrowDown => write!(f, "ArrowDown"),
            Self::ArrowLeft => write!(f, "ArrowLeft"),
            Self::ArrowRight => write!(f, "ArrowRight"),
            Self::Delete => write!(f, "Delete"),
            Self::Backspace => write!(f, "Backspace"),
            Self::Escape => write!(f, "Escape"),
        }
    }
}

pub type ShortcutBindings = HashMap<ShortcutAction, ShortcutBinding>;

pub fn default_binding(action: ShortcutAction) -> ShortcutBinding {
    use ShortcutAction as Action;
    use ShortcutKey as Key;

    match action {
        Action::NewSession => ShortcutBinding::new(Key::Character("n".into()), true, false),
        Action::OpenSession => ShortcutBinding::new(Key::Character("o".into()), true, false),
        Action::SaveSession => ShortcutBinding::new(Key::Character("s".into()), true, false),
        Action::SaveSessionAs => ShortcutBinding::new(Key::Character("s".into()), true, true),
        Action::ImportFiles => ShortcutBinding::new(Key::Character("i".into()), true, false),
        Action::Export => ShortcutBinding::new(Key::Character("e".into()), true, false),
        Action::AddTrack => ShortcutBinding::new(Key::Character("t".into()), true, false),
        Action::SelectAll => ShortcutBinding::new(Key::Character("a".into()), true, false),
        Action::RecordArmToggle => ShortcutBinding::new(Key::Character("r".into()), true, false),
        Action::MidiPanic => ShortcutBinding::new(Key::Character("l".into()), true, false),
        Action::Undo => ShortcutBinding::new(Key::Character("z".into()), true, false),
        Action::Redo => ShortcutBinding::new(Key::Character("z".into()), true, true),
        Action::RedoAlternate => ShortcutBinding::new(Key::Character("y".into()), true, false),
        Action::RemoveSelected => ShortcutBinding::new(Key::Delete, false, false),
        Action::RemoveSelectedBackspace => ShortcutBinding::new(Key::Backspace, false, false),
        Action::Escape => ShortcutBinding::new(Key::Escape, false, false),
        Action::ToggleShortcutsPane => {
            ShortcutBinding::new(Key::Character("s".into()), false, false)
        }
        Action::ToggleModulatorsPane => {
            ShortcutBinding::new(Key::Character("m".into()), false, false)
        }
        Action::ToggleClipsPane => ShortcutBinding::new(Key::Character("c".into()), false, false),
        Action::ToggleCutIndicator => {
            ShortcutBinding::new(Key::Character("x".into()), false, false)
        }
        Action::ToggleSelectedPluginBypass => {
            ShortcutBinding::new(Key::Character("b".into()), false, false)
        }
        Action::ToggleWorkspaceSession => ShortcutBinding::new(Key::Tab, false, false),
        Action::QuantizeSelectedNotes => {
            ShortcutBinding::new(Key::Character("q".into()), false, false)
        }
        Action::HumanizeSelectedNotes => {
            ShortcutBinding::new(Key::Character("h".into()), false, false)
        }
        Action::GrooveSelectedNotes => {
            ShortcutBinding::new(Key::Character("g".into()), false, false)
        }
        Action::ToggleTransport => ShortcutBinding::new(Key::Space, false, false),
        Action::PauseTransport => ShortcutBinding::new(Key::Space, false, true),
        Action::JumpToStart => ShortcutBinding::new(Key::Home, false, false),
        Action::JumpToEnd => ShortcutBinding::new(Key::End, false, false),
        Action::SessionNavUp => ShortcutBinding::new(Key::ArrowUp, false, false),
        Action::SessionNavDown => ShortcutBinding::new(Key::ArrowDown, false, false),
        Action::SessionNavLeft => ShortcutBinding::new(Key::ArrowLeft, false, false),
        Action::SessionNavRight => ShortcutBinding::new(Key::ArrowRight, false, false),
        Action::SessionNavLaunch => ShortcutBinding::new(Key::Return, false, false),
        Action::SessionStopAll => ShortcutBinding::new(Key::Space, false, true),
    }
}

pub fn binding_for(overrides: &ShortcutBindings, action: ShortcutAction) -> ShortcutBinding {
    overrides
        .get(&action)
        .cloned()
        .unwrap_or_else(|| default_binding(action))
}

pub fn action_for_binding(
    binding: &ShortcutBinding,
    overrides: &ShortcutBindings,
    current_view: crate::state::View,
) -> Option<ShortcutAction> {
    shortcut_priority(current_view)
        .into_iter()
        .find(|action| &binding_for(overrides, *action) == binding)
}

fn shortcut_priority(current_view: crate::state::View) -> Vec<ShortcutAction> {
    use ShortcutAction as Action;

    let mut actions = Vec::new();
    if matches!(current_view, crate::state::View::Session) {
        actions.extend([
            Action::SessionStopAll,
            Action::SessionNavUp,
            Action::SessionNavDown,
            Action::SessionNavLeft,
            Action::SessionNavRight,
            Action::SessionNavLaunch,
        ]);
    } else {
        actions.push(Action::PauseTransport);
    }

    actions.extend([
        Action::NewSession,
        Action::OpenSession,
        Action::SaveSessionAs,
        Action::SaveSession,
        Action::ImportFiles,
        Action::Export,
        Action::AddTrack,
        Action::SelectAll,
        Action::RecordArmToggle,
        Action::MidiPanic,
        Action::Redo,
        Action::RedoAlternate,
        Action::Undo,
        Action::RemoveSelected,
        Action::RemoveSelectedBackspace,
        Action::Escape,
        Action::ToggleShortcutsPane,
        Action::ToggleModulatorsPane,
        Action::ToggleClipsPane,
        Action::ToggleCutIndicator,
        Action::ToggleSelectedPluginBypass,
        Action::ToggleWorkspaceSession,
        Action::QuantizeSelectedNotes,
        Action::HumanizeSelectedNotes,
        Action::GrooveSelectedNotes,
        Action::ToggleTransport,
        Action::JumpToStart,
        Action::JumpToEnd,
    ]);
    actions
}