use super::{
canonicalize_shortcut_type, normalize_shortcut, Action, ActionBinding, ActionDefinition,
ActionHostKind, ActionLiveState,
};
use crate::compat::{lock, HashMap, Mutex, String, Vec};
use crate::core::ObjectId;
use crate::shortcut::Shortcut;
use crate::signal::Signal1;
use alloc::sync::Arc;
use core::fmt;
pub struct ActionManager {
actions: HashMap<String, Action>,
shortcut_to_action: HashMap<String, String>,
bindings: Vec<ActionBinding>,
pub action_triggered: Signal1<String>,
pub action_toggled: Signal1<(String, bool)>,
pub action_enabled_changed: Signal1<(String, bool)>,
pub shortcut_conflict: Signal1<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShortcutDisagreement {
pub action_id: String,
pub chord: String,
pub action_manager_has: bool,
pub shortcut_manager_has: bool,
}
impl fmt::Debug for ActionManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ActionManager")
.field("actions", &self.actions.keys())
.field("shortcut_to_action", &self.shortcut_to_action)
.field("bindings", &self.bindings)
.field("action_triggered", &self.action_triggered.slot_count())
.field("action_toggled", &self.action_toggled.slot_count())
.field("action_enabled_changed", &self.action_enabled_changed.slot_count())
.field("shortcut_conflict", &self.shortcut_conflict.slot_count())
.finish()
}
}
impl ActionManager {
pub fn new() -> Self {
Self {
actions: HashMap::new(),
shortcut_to_action: HashMap::new(),
bindings: Vec::new(),
action_triggered: Signal1::new(),
action_toggled: Signal1::new(),
action_enabled_changed: Signal1::new(),
shortcut_conflict: Signal1::new(),
}
}
pub fn register_action(&mut self, id: impl Into<String>, text: impl Into<String>) -> bool {
let id = id.into();
if self.actions.contains_key(&id) {
return false;
}
self.actions.insert(id.clone(), Action::new(id, text));
true
}
pub fn unregister_action(&mut self, id: &str) -> bool {
let removed = self.actions.remove(id).is_some();
if removed {
self.shortcut_to_action.retain(|_, action_id| action_id != id);
self.bindings.retain(|binding| binding.action_id != id);
}
removed
}
pub fn action(&self, id: &str) -> Option<&Action> {
self.actions.get(id)
}
pub fn action_mut(&mut self, id: &str) -> Option<&mut Action> {
self.actions.get_mut(id)
}
pub fn set_action_enabled(&mut self, id: &str, enabled: bool) -> bool {
let Some(action) = self.actions.get_mut(id) else {
return false;
};
let before = action.is_enabled();
action.set_enabled(enabled);
let changed = before != action.is_enabled();
if changed {
let id = String::from(id);
self.action_enabled_changed.emit((id, enabled));
}
true
}
pub fn bind_shortcut(
&mut self,
shortcut: impl Into<String>,
action_id: impl Into<String>,
) -> bool {
let action_id = action_id.into();
if !self.actions.contains_key(&action_id) {
return false;
}
let chord = normalize_shortcut(&shortcut.into());
self.observe_chord_collision(&chord, &action_id);
self.release_shortcuts_for(&action_id);
self.shortcut_to_action.insert(chord, action_id);
true
}
pub fn bind_shortcut_type(
&mut self,
shortcut: &Shortcut,
action_id: impl Into<String>,
) -> bool {
let action_id = action_id.into();
if !self.actions.contains_key(&action_id) {
return false;
}
let chord = canonicalize_shortcut_type(shortcut);
self.observe_chord_collision(&chord, &action_id);
self.release_shortcuts_for(&action_id);
self.shortcut_to_action.insert(chord, action_id);
true
}
fn observe_chord_collision(&self, chord: &str, action_id: &str) {
let Some(existing) = self.shortcut_to_action.get(chord) else {
return;
};
if existing != action_id {
self.shortcut_conflict.emit((String::from(action_id), existing.clone()));
}
}
pub fn report_shortcut_conflict(
&self,
incoming: impl Into<String>,
existing: impl Into<String>,
) {
self.shortcut_conflict.emit((incoming.into(), existing.into()));
}
fn release_shortcuts_for(&mut self, action_id: &str) {
let stale: crate::compat::Vec<String> = self
.shortcut_to_action
.iter()
.filter(|(_, bound)| bound.as_str() == action_id)
.map(|(chord, _)| chord.clone())
.collect();
for chord in stale {
self.shortcut_to_action.remove(&chord);
}
}
pub fn trigger_shortcut(&mut self, shortcut: &str) -> bool {
let Some(action_id) = self.shortcut_to_action.get(&normalize_shortcut(shortcut)).cloned()
else {
return false;
};
self.trigger_action(&action_id)
}
pub fn trigger_shortcut_type(&mut self, shortcut: &Shortcut) -> bool {
let Some(action_id) =
self.shortcut_to_action.get(&canonicalize_shortcut_type(shortcut)).cloned()
else {
return false;
};
self.trigger_action(&action_id)
}
pub fn trigger_action(&mut self, action_id: &str) -> bool {
let Some(action) = self.actions.get_mut(action_id) else {
return false;
};
let was_checked = action.is_checked();
if !action.trigger() {
return false;
}
let checkable = action.is_checkable();
let now_checked = action.is_checked();
let toggled = checkable && now_checked != was_checked;
let id = String::from(action_id);
self.action_triggered.emit(id.clone());
if toggled {
self.action_toggled.emit((id, now_checked));
}
true
}
pub fn action_ids(&self) -> Vec<&String> {
self.actions.keys().collect()
}
pub fn definitions(&self) -> Vec<ActionDefinition> {
let mut definitions: Vec<ActionDefinition> =
self.actions.keys().filter_map(|id| self.action_definition(id)).collect();
definitions.sort_by(|left, right| left.id.cmp(&right.id));
definitions
}
#[cfg(full_widgets)]
pub fn save_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
crate::json::save_actions_to_file(path, &self.definitions())
}
#[cfg(full_widgets)]
pub fn load_from_file(&mut self, path: impl AsRef<std::path::Path>) -> Result<(), String> {
let definitions = crate::json::load_actions_from_file(path)?;
self.register_definitions(&definitions)
}
pub fn register_definitions(&mut self, definitions: &[ActionDefinition]) -> Result<(), String> {
let mut added: Vec<String> = Vec::new();
for definition in definitions {
if !self.register_definition(definition) {
for id in added.iter().rev() {
self.unregister_action(id);
}
return Err(crate::compat::format!(
"action `{}` could not be registered (duplicate id or conflicting chord)",
definition.id
));
}
added.push(definition.id.clone());
}
Ok(())
}
pub fn shortcut_reconciliation(
&self,
shortcuts: &crate::shortcut::ShortcutManager,
) -> Vec<ShortcutDisagreement> {
let mut reported: Vec<String> = Vec::new();
let mut disagreements = Vec::new();
for (shortcut, action_id) in shortcuts.all_shortcuts() {
let chord = canonicalize_shortcut_type(shortcut);
if reported.contains(&chord) {
continue;
}
reported.push(chord.clone());
let indexed = self.shortcut_to_action.get(&chord);
if indexed != Some(action_id) {
disagreements.push(ShortcutDisagreement {
action_id: action_id.clone(),
chord,
action_manager_has: indexed.is_some(),
shortcut_manager_has: true,
});
}
}
for (chord, action_id) in &self.shortcut_to_action {
if reported.contains(chord) {
continue;
}
disagreements.push(ShortcutDisagreement {
action_id: action_id.clone(),
chord: chord.clone(),
action_manager_has: true,
shortcut_manager_has: false,
});
}
disagreements
}
pub fn shortcut_for_action(&self, action_id: &str) -> Option<&String> {
self.shortcut_to_action
.iter()
.find(|(_, bound)| bound.as_str() == action_id)
.map(|(chord, _)| chord)
}
pub fn action_definition(&self, action_id: &str) -> Option<ActionDefinition> {
let action = self.actions.get(action_id)?;
let mut definition = ActionDefinition::from_action(action);
definition.shortcut = self.shortcut_for_action(action_id).cloned().unwrap_or_default();
Some(definition)
}
pub fn watch_action(&self, action_id: &str) -> Option<Arc<Mutex<ActionLiveState>>> {
let definition = self.action_definition(action_id)?;
let state = Arc::new(Mutex::new(ActionLiveState::from_definition(&definition)));
if let Some(action) = self.actions.get(action_id) {
let enabled_state = Arc::clone(&state);
action.connect_enabled_changed_ref(move |enabled| {
lock(&enabled_state).enabled = *enabled;
});
let checked_state = Arc::clone(&state);
action.connect_toggled_ref(move |checked| {
let mut state = lock(&checked_state);
state.checked = *checked;
state.checkable = true;
});
}
Some(state)
}
pub fn register_definition(&mut self, definition: &ActionDefinition) -> bool {
if !self.register_action(definition.id.clone(), definition.text.clone()) {
return false;
}
if let Some(action) = self.actions.get_mut(&definition.id) {
action.set_enabled(definition.enabled);
action.set_checkable(definition.checkable);
if definition.checkable {
action.set_checked(definition.checked);
}
}
if !definition.shortcut.is_empty()
&& !self.bind_shortcut(definition.shortcut.clone(), definition.id.clone())
{
self.unregister_action(&definition.id);
return false;
}
true
}
pub fn bind_action_to_menu(&mut self, action_id: impl Into<String>, menu_id: ObjectId) -> bool {
self.bind_action(action_id.into(), menu_id, ActionHostKind::Menu)
}
pub fn bind_action_to_toolbar(
&mut self,
action_id: impl Into<String>,
toolbar_id: ObjectId,
) -> bool {
self.bind_action(action_id.into(), toolbar_id, ActionHostKind::ToolBar)
}
pub fn bind_action_to_button(
&mut self,
action_id: impl Into<String>,
button_id: ObjectId,
) -> bool {
self.bind_action(action_id.into(), button_id, ActionHostKind::Button)
}
pub fn bindings_for_host(&self, host_id: ObjectId) -> Vec<&ActionBinding> {
self.bindings.iter().filter(|binding| binding.host_id == host_id).collect()
}
fn bind_action(&mut self, action_id: String, host_id: ObjectId, kind: ActionHostKind) -> bool {
if !self.actions.contains_key(&action_id) {
return false;
}
self.bindings.push(ActionBinding { action_id, host_id, kind });
true
}
}
crate::impl_default_via_new!(ActionManager);