mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
1116 lines
44 KiB
Rust
1116 lines
44 KiB
Rust
//! A command catalog with default bindings, the user's overrides, dispatch and conflicts.
|
|
|
|
pub(crate) mod overrides;
|
|
|
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
|
use std::fmt;
|
|
|
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
|
|
use crate::chord::{Chord, Key, Mods, ParseError, Platform, Trigger};
|
|
use crate::command::{CommandRef, MAX_KEY};
|
|
use crate::scope::{Active, Scope, ScopeKind, Scopes};
|
|
|
|
pub use overrides::Overrides;
|
|
pub use overrides::list as overrides_list;
|
|
|
|
/// Most commands a catalog or an [`Overrides`] holds.
|
|
pub const MAX_COMMANDS: usize = 4096;
|
|
/// Most bindings per command.
|
|
pub const MAX_BINDINGS: usize = 16;
|
|
/// Longest binding text, in bytes.
|
|
pub const MAX_BINDING_TEXT: usize = 128;
|
|
/// Most entries [`Keymap::conflicts`] returns.
|
|
pub const MAX_CONFLICTS: usize = 4096;
|
|
|
|
/// What a binding does when pressed.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub enum Kind {
|
|
#[default]
|
|
Command,
|
|
/// Selects a tool. Tools of one group share keys and cycle; tools without a group name form one
|
|
/// group together.
|
|
Tool { group: Option<String> },
|
|
}
|
|
|
|
/// A trigger in a scope. `scope: None` is the command's home scope.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct Binding {
|
|
pub trigger: Trigger,
|
|
pub scope: Option<Scope>,
|
|
}
|
|
|
|
/// Why binding text was refused.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub enum BindingError {
|
|
TooLong,
|
|
/// The part before `": "` is not a scope name.
|
|
Scope,
|
|
Trigger(ParseError),
|
|
}
|
|
|
|
impl fmt::Display for BindingError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
BindingError::TooLong => write!(f, "binding text over {MAX_BINDING_TEXT} bytes"),
|
|
BindingError::Scope => f.write_str("bad scope name"),
|
|
BindingError::Trigger(e) => e.fmt(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for BindingError {}
|
|
|
|
impl Binding {
|
|
/// A binding in the command's home scope.
|
|
pub fn new(trigger: impl Into<Trigger>) -> Binding {
|
|
Binding { trigger: trigger.into(), scope: None }
|
|
}
|
|
|
|
pub fn in_scope(trigger: impl Into<Trigger>, scope: Scope) -> Binding {
|
|
Binding { trigger: trigger.into(), scope: Some(scope) }
|
|
}
|
|
|
|
/// Reads `<trigger>` or `<Scope>: <trigger>`. The whole text is tried as a trigger first, so
|
|
/// `Cmd+: Cmd+:` is the Colon chord pressed twice; otherwise it is split at the first `": "`.
|
|
pub fn parse(text: &str) -> Result<Binding, BindingError> {
|
|
if text.len() > MAX_BINDING_TEXT {
|
|
return Err(BindingError::TooLong);
|
|
}
|
|
let whole = match Trigger::parse(text.trim()) {
|
|
Ok(trigger) => return Ok(Binding::new(trigger)),
|
|
Err(e) => e,
|
|
};
|
|
match text.split_once(": ") {
|
|
Some((scope, trigger)) => {
|
|
let scope = Scope::new(scope.trim()).ok_or(BindingError::Scope)?;
|
|
let trigger = Trigger::parse(trigger.trim()).map_err(BindingError::Trigger)?;
|
|
Ok(Binding::in_scope(trigger, scope))
|
|
}
|
|
None => Err(BindingError::Trigger(whole)),
|
|
}
|
|
}
|
|
|
|
/// The binding text for a command whose home scope is `home`: the scope is written only when
|
|
/// it differs.
|
|
pub fn text(&self, home: &Scope) -> String {
|
|
match &self.scope {
|
|
Some(s) if s != home => format!("{s}: {}", self.trigger),
|
|
_ => self.trigger.to_string(),
|
|
}
|
|
}
|
|
|
|
/// The scope, `home` when unset.
|
|
pub fn scope_or<'a>(&'a self, home: &'a Scope) -> &'a Scope {
|
|
self.scope.as_ref().unwrap_or(home)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Binding {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match &self.scope {
|
|
Some(s) => write!(f, "{s}: {}", self.trigger),
|
|
None => write!(f, "{}", self.trigger),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Serialize for Binding {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
serializer.collect_str(self)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Binding {
|
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Binding, D::Error> {
|
|
let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
|
|
Binding::parse(&text).map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
|
|
/// A command the user can bind, as the app describes it.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Bindable {
|
|
pub command: CommandRef,
|
|
pub label: String,
|
|
/// Menu paths, the main one first; used by search and import.
|
|
#[serde(default)]
|
|
pub paths: Vec<Vec<String>>,
|
|
/// Paths only import matching reads: where foreign sets file the command and the app's menus
|
|
/// do not, such as `["Tools", label]` for a tool. They make no areas or groups in the dialog.
|
|
#[serde(default)]
|
|
pub import_paths: Vec<Vec<String>>,
|
|
/// Other names: labels in other UI languages, older names.
|
|
#[serde(default)]
|
|
pub aliases: Vec<String>,
|
|
/// Home scope.
|
|
pub scope: Scope,
|
|
#[serde(default)]
|
|
pub kind: Kind,
|
|
/// The command shows and hides something (a panel, guides, rulers). Import matching reads a
|
|
/// foreign name without a leading `Show`, `Hide` or `Show/Hide` only for such commands.
|
|
#[serde(default)]
|
|
pub is_toggle: bool,
|
|
#[serde(default)]
|
|
pub defaults: Vec<Binding>,
|
|
}
|
|
|
|
impl Bindable {
|
|
/// A command in `scope` without default bindings.
|
|
pub fn new(command: impl Into<CommandRef>, label: impl Into<String>, scope: Scope) -> Bindable {
|
|
Bindable {
|
|
command: command.into(),
|
|
label: label.into(),
|
|
paths: Vec::new(),
|
|
import_paths: Vec::new(),
|
|
aliases: Vec::new(),
|
|
scope,
|
|
kind: Kind::Command,
|
|
is_toggle: false,
|
|
defaults: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Marks the command as showing and hiding something ([`Bindable::is_toggle`]).
|
|
pub fn toggle(mut self) -> Bindable {
|
|
self.is_toggle = true;
|
|
self
|
|
}
|
|
|
|
pub fn with_kind(mut self, kind: Kind) -> Bindable {
|
|
self.kind = kind;
|
|
self
|
|
}
|
|
|
|
pub fn with_default(mut self, binding: Binding) -> Bindable {
|
|
self.defaults.push(binding);
|
|
self
|
|
}
|
|
|
|
pub fn with_path(mut self, path: Vec<String>) -> Bindable {
|
|
self.paths.push(path);
|
|
self
|
|
}
|
|
|
|
/// Adds a path only import matching reads ([`Bindable::import_paths`]).
|
|
pub fn with_import_path(mut self, path: Vec<String>) -> Bindable {
|
|
self.import_paths.push(path);
|
|
self
|
|
}
|
|
|
|
pub fn with_alias(mut self, alias: impl Into<String>) -> Bindable {
|
|
self.aliases.push(alias.into());
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Which chords work while a text field has the keyboard. Under every rule, the
|
|
/// [`text_editing_chords`] go to the text field too. Bindings in an active context always work: a
|
|
/// `Text: Cmd+B` binding is meant for typing.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub enum TypingRule {
|
|
/// Plain chords (no cmd, ctrl or alt) go to the text; function keys still fire.
|
|
#[default]
|
|
PlainKeysWait,
|
|
/// Only chords with the command modifier and function keys fire.
|
|
CommandKeysOnly,
|
|
/// Nothing fires.
|
|
AllWait,
|
|
}
|
|
|
|
impl TypingRule {
|
|
/// Whether `chord` may fire while typing on `platform`.
|
|
pub fn fires(self, chord: &Chord, platform: Platform) -> bool {
|
|
if text_editing_chords(platform).contains(&chord.effective(platform)) {
|
|
return false;
|
|
}
|
|
match self {
|
|
TypingRule::PlainKeysWait => !chord.is_plain() || chord.is_function_key(),
|
|
TypingRule::CommandKeysOnly => chord.has_command() || chord.is_function_key(),
|
|
TypingRule::AllWait => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
const CMD: Mods = Mods::CMD;
|
|
const CMD_SHIFT: Mods = Mods { shift: true, ..Mods::CMD };
|
|
const ALT: Mods = Mods::ALT;
|
|
const ALT_SHIFT: Mods = Mods { shift: true, ..Mods::ALT };
|
|
const CTRL: Mods = Mods::CTRL;
|
|
|
|
// The chords egui's TextEdit acts on that follow the platform's conventions. egui also takes
|
|
// Ctrl+H, Ctrl+K, Ctrl+U and Ctrl+W everywhere, but off macOS those are app commands.
|
|
const MAC_EDITING: &[Chord] = &[
|
|
// Undo, redo, select all.
|
|
Chord::new(CMD, Key::Z),
|
|
Chord::new(CMD_SHIFT, Key::Z),
|
|
Chord::new(CMD, Key::A),
|
|
// Words.
|
|
Chord::new(ALT, Key::Left),
|
|
Chord::new(ALT, Key::Right),
|
|
Chord::new(ALT_SHIFT, Key::Left),
|
|
Chord::new(ALT_SHIFT, Key::Right),
|
|
Chord::new(ALT, Key::Backspace),
|
|
Chord::new(ALT, Key::Delete),
|
|
// Line start and end.
|
|
Chord::new(CMD, Key::Left),
|
|
Chord::new(CMD, Key::Right),
|
|
Chord::new(CMD_SHIFT, Key::Left),
|
|
Chord::new(CMD_SHIFT, Key::Right),
|
|
Chord::new(CMD, Key::Backspace),
|
|
Chord::new(CMD, Key::Delete),
|
|
// Text start and end.
|
|
Chord::new(CMD, Key::Up),
|
|
Chord::new(CMD, Key::Down),
|
|
Chord::new(CMD_SHIFT, Key::Up),
|
|
Chord::new(CMD_SHIFT, Key::Down),
|
|
// The Control keys of Cocoa text fields: line start and end, character and line moves,
|
|
// deletion.
|
|
Chord::new(CTRL, Key::A),
|
|
Chord::new(CTRL, Key::E),
|
|
Chord::new(CTRL, Key::B),
|
|
Chord::new(CTRL, Key::F),
|
|
Chord::new(CTRL, Key::P),
|
|
Chord::new(CTRL, Key::N),
|
|
Chord::new(CTRL, Key::H),
|
|
Chord::new(CTRL, Key::K),
|
|
Chord::new(CTRL, Key::U),
|
|
Chord::new(CTRL, Key::W),
|
|
];
|
|
|
|
const PC_EDITING: &[Chord] = &[
|
|
// Undo, redo, select all.
|
|
Chord::new(CMD, Key::Z),
|
|
Chord::new(CMD, Key::Y),
|
|
Chord::new(CMD_SHIFT, Key::Z),
|
|
Chord::new(CMD, Key::A),
|
|
// Words.
|
|
Chord::new(CMD, Key::Left),
|
|
Chord::new(CMD, Key::Right),
|
|
Chord::new(CMD_SHIFT, Key::Left),
|
|
Chord::new(CMD_SHIFT, Key::Right),
|
|
Chord::new(CMD, Key::Backspace),
|
|
Chord::new(CMD, Key::Delete),
|
|
// Text start and end.
|
|
Chord::new(CMD, Key::Home),
|
|
Chord::new(CMD, Key::End),
|
|
Chord::new(CMD_SHIFT, Key::Home),
|
|
Chord::new(CMD_SHIFT, Key::End),
|
|
Chord::new(CMD, Key::Up),
|
|
Chord::new(CMD, Key::Down),
|
|
Chord::new(CMD_SHIFT, Key::Up),
|
|
Chord::new(CMD_SHIFT, Key::Down),
|
|
];
|
|
|
|
/// The chords with modifiers that edit text on `platform`, as effective chords: undo, redo,
|
|
/// select all, and moving and deleting by word, line and text. While typing they go to the text
|
|
/// field under every [`TypingRule`]; only a binding in an active context takes one. Plain keys
|
|
/// (arrows, Home, End, Backspace, Delete, with or without Shift) are not listed, since every rule
|
|
/// holds them already. The `egui` dispatcher keeps the clipboard chords for the field itself.
|
|
pub fn text_editing_chords(platform: Platform) -> &'static [Chord] {
|
|
if platform.is_mac() { MAC_EDITING } else { PC_EDITING }
|
|
}
|
|
|
|
/// What a key press runs.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
#[non_exhaustive]
|
|
pub enum Lookup {
|
|
None,
|
|
Run(CommandRef),
|
|
/// Tools of one group share the chord: the app picks the next one (catalog order).
|
|
Cycle(Vec<CommandRef>),
|
|
}
|
|
|
|
/// What to do with other commands that hold a chord being assigned.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum Policy {
|
|
/// Remove the chord from them.
|
|
#[default]
|
|
Steal,
|
|
/// Assign anyway; both keep it.
|
|
Keep,
|
|
/// Do not assign.
|
|
Refuse,
|
|
}
|
|
|
|
/// A command's binding.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Holder {
|
|
pub command: CommandRef,
|
|
pub binding: Binding,
|
|
}
|
|
|
|
/// The result of an assignment.
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub struct Assigned {
|
|
/// Bindings removed from other commands ([`Policy::Steal`]).
|
|
pub taken_from: Vec<Holder>,
|
|
/// Bindings of other commands that now clash ([`Policy::Keep`]).
|
|
pub clashes: Vec<Holder>,
|
|
}
|
|
|
|
/// Why an assignment was refused.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub enum AssignError {
|
|
UnknownCommand,
|
|
/// The system takes this chord.
|
|
Reserved(Trigger),
|
|
/// More than [`MAX_BINDINGS`] bindings.
|
|
TooMany,
|
|
/// Other commands hold the chord ([`Policy::Refuse`]).
|
|
Taken(Vec<Holder>),
|
|
/// A scope that is neither registered in [`Scopes`] nor used by the catalog.
|
|
UnknownScope(Scope),
|
|
/// The change needs a new [`Overrides`] entry and it already holds [`MAX_COMMANDS`].
|
|
Full,
|
|
}
|
|
|
|
impl fmt::Display for AssignError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
AssignError::UnknownCommand => f.write_str("unknown command"),
|
|
AssignError::Reserved(t) => write!(f, "{t} is reserved by the system"),
|
|
AssignError::TooMany => write!(f, "more than {MAX_BINDINGS} bindings"),
|
|
AssignError::Taken(h) => write!(f, "in use by {} other command(s)", h.len()),
|
|
AssignError::UnknownScope(s) => write!(f, "unknown scope {s}"),
|
|
AssignError::Full => write!(f, "more than {MAX_COMMANDS} changed commands"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for AssignError {}
|
|
|
|
/// Why loading dropped part of an [`Overrides`] entry.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub enum DropReason {
|
|
KeyTooLong,
|
|
/// The value is not a list of texts.
|
|
NotAList,
|
|
/// Items of the list that are not text.
|
|
NotText,
|
|
TextTooLong,
|
|
/// Texts past [`MAX_BINDINGS`].
|
|
TooManyBindings,
|
|
}
|
|
|
|
/// Something wrong in the stored overrides. Load reports these instead of failing.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub enum Problem {
|
|
/// No command in the catalog has this key; the entry is kept.
|
|
UnknownCommand { key: String },
|
|
/// A text that does not parse; the entry keeps it.
|
|
BadBinding { key: String, text: String, error: BindingError },
|
|
/// A reserved chord; not bound.
|
|
Reserved { key: String, text: String },
|
|
/// A scope that is neither registered in [`Scopes`] nor used by the catalog; not bound.
|
|
UnknownScope { key: String, text: String },
|
|
/// Another entry names the same command; this one is ignored.
|
|
DuplicateKey { key: String },
|
|
/// Part of an entry dropped while loading.
|
|
Dropped { key: String, reason: DropReason },
|
|
/// Entries past [`MAX_COMMANDS`] dropped while loading.
|
|
TooManyCommands { ignored: usize },
|
|
}
|
|
|
|
/// How two bindings on one chord relate.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub enum ConflictKind {
|
|
/// Only one of the two can run.
|
|
Clash,
|
|
/// Both work: the nearer scope runs where it applies, the other elsewhere.
|
|
Shadow,
|
|
}
|
|
|
|
/// Two commands on one chord.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[non_exhaustive]
|
|
pub struct Conflict {
|
|
/// The effective trigger on the keymap's platform.
|
|
pub chord: Trigger,
|
|
pub a: (CommandRef, Binding),
|
|
pub b: (CommandRef, Binding),
|
|
pub kind: ConflictKind,
|
|
/// Both bindings are defaults.
|
|
pub in_defaults: bool,
|
|
}
|
|
|
|
/// A binding with its scope resolved.
|
|
type Bound = (Trigger, Scope);
|
|
|
|
/// A command's bindings on one chord grouped by scope and tool group, for [`Keymap::conflicts`].
|
|
struct Group<'a> {
|
|
scope: &'a Scope,
|
|
/// The tool group for tools, `None` for other commands.
|
|
tools: Option<&'a Option<String>>,
|
|
/// (position in the chord's list, command, binding).
|
|
items: Vec<(usize, usize, &'a Bound)>,
|
|
}
|
|
|
|
/// Two holders of one chord: their positions in the chord's list, whether they clash, and each
|
|
/// one's command and binding.
|
|
type Pair<'a> = (usize, usize, bool, (usize, &'a Bound), (usize, &'a Bound));
|
|
|
|
/// The catalog, the scope registry, the platform and the user's overrides.
|
|
#[derive(Clone, Debug)]
|
|
pub struct Keymap {
|
|
catalog: Vec<Bindable>,
|
|
keys: Vec<String>,
|
|
index: HashMap<String, usize>,
|
|
defaults: Vec<Vec<Bound>>,
|
|
current: Vec<Vec<Bound>>,
|
|
/// Effective trigger → (command, position in its `current` list), kept in step with `current`.
|
|
by_trigger: HashMap<Trigger, BTreeSet<(usize, usize)>>,
|
|
/// The overrides entry each command reads, when it has one.
|
|
stored: Vec<Option<String>>,
|
|
/// Whether the overrides hold a hand-written variant of a command's key
|
|
/// (`window.panel { "panel": "layers" }`); only loading adds them.
|
|
variants: bool,
|
|
/// Home and default scopes of the catalog.
|
|
catalog_scopes: HashSet<Scope>,
|
|
scopes: Scopes,
|
|
platform: Platform,
|
|
typing: TypingRule,
|
|
reserved: Vec<Chord>,
|
|
overrides: Overrides,
|
|
}
|
|
|
|
impl Keymap {
|
|
/// A keymap with default bindings. Entries past [`MAX_COMMANDS`], with a key over
|
|
/// [`MAX_KEY`] bytes, or repeating an earlier key are left out.
|
|
pub fn new(catalog: Vec<Bindable>, scopes: Scopes, platform: Platform) -> Keymap {
|
|
let mut km = Keymap {
|
|
catalog: Vec::new(),
|
|
keys: Vec::new(),
|
|
index: HashMap::new(),
|
|
defaults: Vec::new(),
|
|
current: Vec::new(),
|
|
by_trigger: HashMap::new(),
|
|
stored: Vec::new(),
|
|
variants: false,
|
|
catalog_scopes: HashSet::new(),
|
|
scopes,
|
|
platform,
|
|
typing: TypingRule::default(),
|
|
reserved: Vec::new(),
|
|
overrides: Overrides::new(),
|
|
};
|
|
for b in catalog {
|
|
let key = b.command.key();
|
|
if km.catalog.len() >= MAX_COMMANDS || key.len() > MAX_KEY || km.index.contains_key(&key) {
|
|
continue;
|
|
}
|
|
let defaults = km.normalize(b.defaults.iter().cloned(), &b.scope);
|
|
km.catalog_scopes.insert(b.scope.clone());
|
|
km.catalog_scopes.extend(defaults.iter().map(|(_, s)| s.clone()));
|
|
km.index.insert(key.clone(), km.catalog.len());
|
|
km.keys.push(key);
|
|
km.current.push(defaults.clone());
|
|
km.defaults.push(defaults);
|
|
km.stored.push(None);
|
|
km.catalog.push(b);
|
|
}
|
|
km.rebuild_index();
|
|
km
|
|
}
|
|
|
|
pub fn with_typing_rule(mut self, rule: TypingRule) -> Keymap {
|
|
self.typing = rule;
|
|
self
|
|
}
|
|
|
|
/// Chords that cannot be assigned or bound, e.g. [`reserved::system`](crate::reserved::system).
|
|
/// Without this call the keymap reserves no chord.
|
|
pub fn with_reserved(mut self, chords: Vec<Chord>) -> Keymap {
|
|
self.reserved = chords;
|
|
self.reload();
|
|
self
|
|
}
|
|
|
|
pub fn with_overrides(mut self, overrides: Overrides) -> Keymap {
|
|
self.set_overrides(overrides);
|
|
self
|
|
}
|
|
|
|
pub fn set_overrides(&mut self, overrides: Overrides) {
|
|
self.overrides = overrides;
|
|
self.reload();
|
|
}
|
|
|
|
pub fn overrides(&self) -> &Overrides {
|
|
&self.overrides
|
|
}
|
|
|
|
pub fn platform(&self) -> Platform {
|
|
self.platform
|
|
}
|
|
|
|
pub fn scopes(&self) -> &Scopes {
|
|
&self.scopes
|
|
}
|
|
|
|
pub fn typing_rule(&self) -> TypingRule {
|
|
self.typing
|
|
}
|
|
|
|
pub fn catalog(&self) -> &[Bindable] {
|
|
&self.catalog
|
|
}
|
|
|
|
pub fn bindable(&self, command: &CommandRef) -> Option<&Bindable> {
|
|
self.find(command).map(|i| &self.catalog[i])
|
|
}
|
|
|
|
/// The effective bindings of a command; empty for an unknown one.
|
|
pub fn bindings(&self, command: &CommandRef) -> Vec<Binding> {
|
|
self.find(command).map(|i| self.current[i].iter().map(|b| self.public(i, b)).collect()).unwrap_or_default()
|
|
}
|
|
|
|
/// The default bindings of a command; empty for an unknown one.
|
|
pub fn default_bindings(&self, command: &CommandRef) -> Vec<Binding> {
|
|
self.find(command).map(|i| self.defaults[i].iter().map(|b| self.public(i, b)).collect()).unwrap_or_default()
|
|
}
|
|
|
|
pub fn is_reserved(&self, chord: &Chord) -> bool {
|
|
let c = chord.effective(self.platform);
|
|
self.reserved.iter().any(|r| r.effective(self.platform) == c)
|
|
}
|
|
|
|
/// Whether bindings may use `scope`: the application, a scope registered in [`Scopes`], or a
|
|
/// home or default scope of the catalog.
|
|
pub fn knows_scope(&self, scope: &Scope) -> bool {
|
|
scope.is_app() || self.scopes.knows(scope) || self.catalog_scopes.contains(scope)
|
|
}
|
|
|
|
/// The command `chord` (pressed twice when `double`) runs with `active` focus.
|
|
pub fn lookup(&self, chord: &Chord, double: bool, active: &Active) -> Lookup {
|
|
let want = Trigger { chord: chord.effective(self.platform), double };
|
|
let held = active.typing && !self.typing.fires(&want.chord, self.platform);
|
|
let Some(holders) = self.by_trigger.get(&want) else { return Lookup::None };
|
|
// Each command's nearest rank; `holders` is in catalog order.
|
|
let mut ranks: Vec<(usize, u8)> = Vec::new();
|
|
for &(i, k) in holders {
|
|
let Some(r) = self.at(i, k).and_then(|(_, s)| self.scopes.rank(s, active)) else { continue };
|
|
// The typing rule holds back everything except bindings of an active context (rank 0).
|
|
if held && r != 0 {
|
|
continue;
|
|
}
|
|
match ranks.last_mut() {
|
|
Some((j, best)) if *j == i => *best = (*best).min(r),
|
|
_ => ranks.push((i, r)),
|
|
}
|
|
}
|
|
let Some(best) = ranks.iter().map(|(_, r)| *r).min() else { return Lookup::None };
|
|
let winners: Vec<usize> = ranks.iter().filter(|(_, r)| *r == best).map(|(i, _)| *i).collect();
|
|
match winners.as_slice() {
|
|
[] => Lookup::None,
|
|
[i] => Lookup::Run(self.catalog[*i].command.clone()),
|
|
[first, rest @ ..] => {
|
|
if rest.iter().all(|j| self.same_group(*first, *j)) {
|
|
Lookup::Cycle(winners.iter().map(|i| self.catalog[*i].command.clone()).collect())
|
|
} else {
|
|
Lookup::Run(self.catalog[*first].command.clone())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Adds a binding to a command.
|
|
pub fn assign(&mut self, command: &CommandRef, binding: Binding, policy: Policy) -> Result<Assigned, AssignError> {
|
|
let i = self.find(command).ok_or(AssignError::UnknownCommand)?;
|
|
let new = self.bound(i, &binding);
|
|
if self.current[i].iter().any(|b| self.same(b, &new)) {
|
|
return Ok(Assigned::default());
|
|
}
|
|
let mut list = self.current[i].clone();
|
|
list.push(new.clone());
|
|
self.apply(i, list, &[new], policy)
|
|
}
|
|
|
|
/// Replaces all bindings of a command.
|
|
pub fn set_bindings(&mut self, command: &CommandRef, bindings: Vec<Binding>, policy: Policy) -> Result<Assigned, AssignError> {
|
|
let i = self.find(command).ok_or(AssignError::UnknownCommand)?;
|
|
if bindings.len() > MAX_BINDINGS {
|
|
return Err(AssignError::TooMany);
|
|
}
|
|
let home = self.catalog[i].scope.clone();
|
|
let list = self.normalize(bindings, &home);
|
|
let new: Vec<Bound> = list.iter().filter(|b| !self.current[i].iter().any(|c| self.same(c, b))).cloned().collect();
|
|
self.apply(i, list, &new, policy)
|
|
}
|
|
|
|
/// Removes one binding (matched by effective trigger and scope). `false` when the command does
|
|
/// not have it, or when the change needs a new [`Overrides`] entry and the overrides are full
|
|
/// ([`AssignError::Full`]); nothing changes then.
|
|
pub fn remove(&mut self, command: &CommandRef, binding: &Binding) -> bool {
|
|
let Some(i) = self.find(command) else { return false };
|
|
let gone = self.bound(i, binding);
|
|
let before = self.current[i].len();
|
|
let list: Vec<Bound> = self.current[i].iter().filter(|b| !self.same(b, &gone)).cloned().collect();
|
|
list.len() != before && self.commit(vec![(i, list)]).is_ok()
|
|
}
|
|
|
|
/// Leaves a command without shortcuts, in every scope. `false` for an unknown command, or when
|
|
/// the overrides are full as for [`Keymap::remove`].
|
|
pub fn clear(&mut self, command: &CommandRef) -> bool {
|
|
let Some(i) = self.find(command) else { return false };
|
|
self.commit(vec![(i, Vec::new())]).is_ok()
|
|
}
|
|
|
|
/// Gives a command its default bindings back.
|
|
pub fn reset(&mut self, command: &CommandRef) -> bool {
|
|
let Some(i) = self.find(command) else { return false };
|
|
// The whole entry goes, texts this build cannot use included.
|
|
self.remove_entries(i);
|
|
self.set_current(i, self.defaults[i].clone());
|
|
true
|
|
}
|
|
|
|
/// Every command gets its defaults; unknown entries go too.
|
|
pub fn reset_all(&mut self) {
|
|
self.overrides.clear();
|
|
self.reload();
|
|
}
|
|
|
|
/// Pairs of commands on one chord, at most [`MAX_CONFLICTS`], in chord order, then in
|
|
/// catalog order. Tools of one group and bindings of panels that are never active together
|
|
/// are left out; with panel fallthrough two unrelated panels on one chord tie in a third panel,
|
|
/// which is reported as a [`ConflictKind::Shadow`].
|
|
///
|
|
/// When the cap cuts a chord's pairs, the pairs kept are the first found comparing the
|
|
/// chord's holders grouped by scope and tool group, group by group in the order each group
|
|
/// first appears in the catalog; the kept pairs are still returned in chord order, then
|
|
/// catalog order.
|
|
pub fn conflicts(&self) -> Vec<Conflict> {
|
|
let mut chords: Vec<(&Trigger, &BTreeSet<(usize, usize)>)> = self.by_trigger.iter().filter(|(_, h)| h.len() > 1).collect();
|
|
chords.sort_by_key(|(t, _)| **t);
|
|
let mut out = Vec::new();
|
|
for (chord, holders) in chords {
|
|
if !self.chord_conflicts(*chord, holders, &mut out) {
|
|
break;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The conflicts [`Keymap::conflicts`] reports for one command's bindings, with that command
|
|
/// as `a`, in the order of its bindings, then catalog order. Empty for an unknown command.
|
|
pub fn conflicts_with(&self, command: &CommandRef) -> Vec<Conflict> {
|
|
let Some(i) = self.find(command) else { return Vec::new() };
|
|
let mut out = Vec::new();
|
|
for a in &self.current[i] {
|
|
let chord = a.0.effective(self.platform);
|
|
let Some(holders) = self.by_trigger.get(&chord) else { continue };
|
|
for &(j, k) in holders {
|
|
if j == i || self.same_group(i, j) {
|
|
continue;
|
|
}
|
|
let Some(b) = self.at(j, k) else { continue };
|
|
let Some(clash) = self.conflict_relation(&a.1, &b.1) else { continue };
|
|
out.push(Conflict {
|
|
chord,
|
|
a: (self.catalog[i].command.clone(), self.public(i, a)),
|
|
b: (self.catalog[j].command.clone(), self.public(j, b)),
|
|
kind: if clash { ConflictKind::Clash } else { ConflictKind::Shadow },
|
|
in_defaults: self.defaults[i].contains(a) && self.defaults[j].contains(b),
|
|
});
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// What is wrong with the stored overrides: load notes, unknown commands, texts that do not
|
|
/// parse, reserved chords, unknown scopes, duplicate entries.
|
|
pub fn problems(&self) -> Vec<Problem> {
|
|
let mut out = self.overrides.load_notes().to_vec();
|
|
for (key, texts) in self.overrides.iter() {
|
|
let Some(i) = self.resolve_key(key) else {
|
|
out.push(Problem::UnknownCommand { key: key.to_string() });
|
|
continue;
|
|
};
|
|
if self.stored[i].as_deref() != Some(key) {
|
|
out.push(Problem::DuplicateKey { key: key.to_string() });
|
|
continue;
|
|
}
|
|
let home = &self.catalog[i].scope;
|
|
for text in texts {
|
|
let (key, text) = (key.to_string(), text.clone());
|
|
match Binding::parse(&text) {
|
|
Err(error) => out.push(Problem::BadBinding { key, text, error }),
|
|
Ok(b) if self.is_reserved(&b.trigger.chord) => out.push(Problem::Reserved { key, text }),
|
|
Ok(b) if !self.usable_scope(b.scope_or(home), home) => out.push(Problem::UnknownScope { key, text }),
|
|
Ok(_) => {}
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Bindings of other commands that only one of them and `bindings` on `command` could run,
|
|
/// leaving out tools of the same group and twins (as [`Policy::Steal`] does).
|
|
pub(crate) fn clashing(&self, command: &CommandRef, bindings: &[Binding]) -> Vec<Holder> {
|
|
let Some(i) = self.find(command) else { return Vec::new() };
|
|
let new: Vec<Bound> = bindings.iter().map(|b| self.bound(i, b)).collect();
|
|
self.clashes(i, &new).into_iter().map(|(j, b)| Holder { command: self.catalog[j].command.clone(), binding: self.public(j, &b) }).collect()
|
|
}
|
|
|
|
/// Whether binding `x` of command `a` and binding `y` of another command `b` clash, by the
|
|
/// rules of [`Keymap::clashing`].
|
|
pub(crate) fn pair_clashes(&self, a: &CommandRef, x: &Binding, b: &CommandRef, y: &Binding) -> bool {
|
|
let (Some(i), Some(j)) = (self.find(a), self.find(b)) else { return false };
|
|
if i == j || self.same_group(i, j) {
|
|
return false;
|
|
}
|
|
let (bx, by) = (self.bound(i, x), self.bound(j, y));
|
|
let t = bx.0.effective(self.platform);
|
|
t == by.0.effective(self.platform) && !self.twins(i, j, t) && self.scopes.relation(&bx.1, &by.1) == Some(true)
|
|
}
|
|
|
|
/// Whether both commands of `conflict` have its chord among their defaults, so they share it by
|
|
/// design and [`Keymap::clashing`] leaves them out.
|
|
#[cfg(feature = "ui")]
|
|
pub(crate) fn twins_in(&self, conflict: &Conflict) -> bool {
|
|
let (Some(i), Some(j)) = (self.find(&conflict.a.0), self.find(&conflict.b.0)) else { return false };
|
|
self.twins(i, j, conflict.chord)
|
|
}
|
|
|
|
fn find(&self, command: &CommandRef) -> Option<usize> {
|
|
self.index.get(&command.key()).copied()
|
|
}
|
|
|
|
fn resolve_key(&self, key: &str) -> Option<usize> {
|
|
self.index.get(key).or_else(|| CommandRef::parse_key(key).and_then(|c| self.index.get(&c.key()))).copied()
|
|
}
|
|
|
|
fn at(&self, i: usize, k: usize) -> Option<&Bound> {
|
|
self.current.get(i).and_then(|l| l.get(k))
|
|
}
|
|
|
|
fn bound(&self, i: usize, binding: &Binding) -> Bound {
|
|
(binding.trigger, binding.scope_or(&self.catalog[i].scope).clone())
|
|
}
|
|
|
|
fn public(&self, i: usize, (trigger, scope): &Bound) -> Binding {
|
|
let scope = (*scope != self.catalog[i].scope).then(|| scope.clone());
|
|
Binding { trigger: *trigger, scope }
|
|
}
|
|
|
|
fn same(&self, a: &Bound, b: &Bound) -> bool {
|
|
a.1 == b.1 && a.0.effective(self.platform) == b.0.effective(self.platform)
|
|
}
|
|
|
|
/// Whether a stored binding of a command with home scope `home` may use `scope`.
|
|
fn usable_scope(&self, scope: &Scope, home: &Scope) -> bool {
|
|
scope == home || self.knows_scope(scope)
|
|
}
|
|
|
|
/// Resolves scopes, drops repeats and caps the count.
|
|
fn normalize(&self, bindings: impl IntoIterator<Item = Binding>, home: &Scope) -> Vec<Bound> {
|
|
let mut out: Vec<Bound> = Vec::new();
|
|
for b in bindings {
|
|
let bound = (b.trigger, b.scope_or(home).clone());
|
|
if out.len() < MAX_BINDINGS && !out.contains(&bound) {
|
|
out.push(bound);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn same_group(&self, i: usize, j: usize) -> bool {
|
|
match (&self.catalog[i].kind, &self.catalog[j].kind) {
|
|
(Kind::Tool { group: a }, Kind::Tool { group: b }) => a == b,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Both commands have `trigger` among their defaults: they share it by design.
|
|
fn twins(&self, i: usize, j: usize, trigger: Trigger) -> bool {
|
|
let has = |k: usize| self.defaults[k].iter().any(|(t, _)| t.effective(self.platform) == trigger);
|
|
has(i) && has(j)
|
|
}
|
|
|
|
/// Bindings of other commands that only one of `new` and them could run.
|
|
fn clashes(&self, i: usize, new: &[Bound]) -> Vec<(usize, Bound)> {
|
|
let mut out = Vec::new();
|
|
let mut seen: HashSet<(usize, usize)> = HashSet::new();
|
|
for (t, s) in new {
|
|
let t = t.effective(self.platform);
|
|
let Some(holders) = self.by_trigger.get(&t) else { continue };
|
|
for &(j, k) in holders {
|
|
if j == i || self.same_group(i, j) || self.twins(i, j, t) {
|
|
continue;
|
|
}
|
|
let Some(b) = self.at(j, k) else { continue };
|
|
if self.scopes.relation(s, &b.1) == Some(true) && seen.insert((j, k)) {
|
|
out.push((j, b.clone()));
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// How two scopes on one chord relate for [`Keymap::conflicts`]: [`Scopes::relation`], and with
|
|
/// panel fallthrough two unrelated panels shadow (they tie in any third panel).
|
|
fn conflict_relation(&self, a: &Scope, b: &Scope) -> Option<bool> {
|
|
self.scopes.relation(a, b).or_else(|| {
|
|
let panels = self.scopes.kind(a) == ScopeKind::Panel && self.scopes.kind(b) == ScopeKind::Panel;
|
|
(panels && self.scopes.panel_fallthrough()).then_some(false)
|
|
})
|
|
}
|
|
|
|
/// Adds the conflicts on one chord to `out`; `false` once `out` is full.
|
|
///
|
|
/// The holders are grouped by scope and tool group, and only groups that relate are compared,
|
|
/// so pairs that are left out (unrelated panels, tools of one group) cost nothing per pair.
|
|
fn chord_conflicts(&self, chord: Trigger, holders: &BTreeSet<(usize, usize)>, out: &mut Vec<Conflict>) -> bool {
|
|
let mut groups: Vec<Group> = Vec::new();
|
|
let mut group_of: HashMap<(&Scope, Option<&Option<String>>), usize> = HashMap::new();
|
|
for (pos, &(i, k)) in holders.iter().enumerate() {
|
|
let Some(b) = self.at(i, k) else { continue };
|
|
let tools = match &self.catalog[i].kind {
|
|
Kind::Tool { group } => Some(group),
|
|
_ => None,
|
|
};
|
|
let g = *group_of.entry((&b.1, tools)).or_insert_with(|| {
|
|
groups.push(Group { scope: &b.1, tools, items: Vec::new() });
|
|
groups.len() - 1
|
|
});
|
|
if let Some(group) = groups.get_mut(g) {
|
|
group.items.push((pos, i, b));
|
|
}
|
|
}
|
|
let room = MAX_CONFLICTS.saturating_sub(out.len());
|
|
let mut found: Vec<Pair> = Vec::new();
|
|
'groups: for (n, g) in groups.iter().enumerate() {
|
|
for (m, h) in groups.iter().enumerate().skip(n) {
|
|
if g.tools.is_some() && g.tools == h.tools {
|
|
continue;
|
|
}
|
|
let Some(clash) = self.conflict_relation(g.scope, h.scope) else { continue };
|
|
let same = m == n;
|
|
for (x, &(pa, i, a)) in g.items.iter().enumerate() {
|
|
let rest = if same { g.items.get(x + 1..).unwrap_or_default() } else { &h.items };
|
|
for &(pb, j, b) in rest {
|
|
if i == j {
|
|
continue;
|
|
}
|
|
if found.len() >= room {
|
|
break 'groups;
|
|
}
|
|
let pair = if pa < pb { (pa, pb, clash, (i, a), (j, b)) } else { (pb, pa, clash, (j, b), (i, a)) };
|
|
found.push(pair);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
found.sort_by_key(|(pa, pb, ..)| (*pa, *pb));
|
|
for (_, _, clash, (i, a), (j, b)) in found {
|
|
out.push(Conflict {
|
|
chord,
|
|
a: (self.catalog[i].command.clone(), self.public(i, a)),
|
|
b: (self.catalog[j].command.clone(), self.public(j, b)),
|
|
kind: if clash { ConflictKind::Clash } else { ConflictKind::Shadow },
|
|
in_defaults: self.defaults[i].contains(a) && self.defaults[j].contains(b),
|
|
});
|
|
}
|
|
out.len() < MAX_CONFLICTS
|
|
}
|
|
|
|
fn apply(&mut self, i: usize, list: Vec<Bound>, new: &[Bound], policy: Policy) -> Result<Assigned, AssignError> {
|
|
if list.len() > MAX_BINDINGS {
|
|
return Err(AssignError::TooMany);
|
|
}
|
|
if let Some((t, _)) = new.iter().find(|(t, _)| self.is_reserved(&t.chord)) {
|
|
return Err(AssignError::Reserved(*t));
|
|
}
|
|
let home = &self.catalog[i].scope;
|
|
if let Some((_, s)) = new.iter().find(|(_, s)| !self.usable_scope(s, home)) {
|
|
return Err(AssignError::UnknownScope(s.clone()));
|
|
}
|
|
let clashes = self.clashes(i, new);
|
|
let holders: Vec<Holder> =
|
|
clashes.iter().map(|(j, b)| Holder { command: self.catalog[*j].command.clone(), binding: self.public(*j, b) }).collect();
|
|
let mut done = Assigned::default();
|
|
let mut writes: Vec<(usize, Vec<Bound>)> = Vec::new();
|
|
match policy {
|
|
Policy::Refuse if !holders.is_empty() => return Err(AssignError::Taken(holders)),
|
|
Policy::Refuse => {}
|
|
Policy::Keep => done.clashes = holders,
|
|
Policy::Steal => {
|
|
let mut victims: Vec<usize> = Vec::new();
|
|
let mut gone: HashMap<usize, Vec<&Bound>> = HashMap::new();
|
|
for (j, b) in &clashes {
|
|
let list = gone.entry(*j).or_default();
|
|
if list.is_empty() {
|
|
victims.push(*j);
|
|
}
|
|
list.push(b);
|
|
}
|
|
for j in victims {
|
|
let gone = gone.get(&j).map(Vec::as_slice).unwrap_or_default();
|
|
let rest: Vec<Bound> = self.current[j].iter().filter(|o| !gone.contains(o)).cloned().collect();
|
|
writes.push((j, rest));
|
|
}
|
|
done.taken_from = holders;
|
|
}
|
|
}
|
|
writes.push((i, list));
|
|
self.commit(writes)?;
|
|
Ok(done)
|
|
}
|
|
|
|
/// Stores several commands' bindings, or none of them when the overrides lack room for the
|
|
/// new entries.
|
|
fn commit(&mut self, writes: Vec<(usize, Vec<Bound>)>) -> Result<(), AssignError> {
|
|
let needed = writes.iter().filter(|(i, list)| self.stored[*i].is_none() && !self.is_default(*i, list)).count();
|
|
if self.overrides.len() + needed > MAX_COMMANDS {
|
|
return Err(AssignError::Full);
|
|
}
|
|
for (i, list) in writes {
|
|
self.store(i, list);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Writes a command's bindings to the overrides, dropping the entry when they equal the
|
|
/// defaults. Texts of the old entry this build cannot use stay in it, after the bindings and up
|
|
/// to [`MAX_BINDINGS`], unless the command is left without bindings: an entry of only such
|
|
/// texts loads as the defaults. Callers check for room first ([`Keymap::commit`]); texts always
|
|
/// fit, since a scope name, `": "` and the longest double press stay under
|
|
/// [`MAX_BINDING_TEXT`].
|
|
fn store(&mut self, i: usize, list: Vec<Bound>) {
|
|
let mut kept = if list.is_empty() { Vec::new() } else { self.unusable_texts(i) };
|
|
kept.truncate(MAX_BINDINGS.saturating_sub(list.len()));
|
|
self.remove_entries(i);
|
|
if !kept.is_empty() || !self.is_default(i, &list) {
|
|
let home = &self.catalog[i].scope;
|
|
let mut texts: Vec<String> = list.iter().map(|b| self.public(i, b).text(home)).collect();
|
|
texts.extend(kept);
|
|
if self.overrides.insert(self.keys[i].clone(), texts) {
|
|
self.stored[i] = Some(self.keys[i].clone());
|
|
}
|
|
}
|
|
self.set_current(i, list);
|
|
}
|
|
|
|
/// Removes a command's overrides entries: the one it reads and hand-written variants of its
|
|
/// key, which would take effect on the next load.
|
|
fn remove_entries(&mut self, i: usize) {
|
|
if let Some(old) = self.stored[i].take() {
|
|
self.overrides.remove(&old);
|
|
}
|
|
if !self.variants {
|
|
return;
|
|
}
|
|
let keys: Vec<String> = self.overrides.iter().map(|(k, _)| k).filter(|k| self.resolve_key(k) == Some(i)).map(str::to_string).collect();
|
|
for key in keys {
|
|
self.overrides.remove(&key);
|
|
}
|
|
}
|
|
|
|
/// Whether `list` is the command's defaults, comparing effective triggers.
|
|
fn is_default(&self, i: usize, list: &[Bound]) -> bool {
|
|
let defaults = &self.defaults[i];
|
|
list.len() == defaults.len() && list.iter().zip(defaults).all(|(a, b)| self.same(a, b))
|
|
}
|
|
|
|
/// Whether a stored binding of a command with home scope `home` works here.
|
|
fn usable(&self, binding: &Binding, home: &Scope) -> bool {
|
|
!self.is_reserved(&binding.trigger.chord) && self.usable_scope(binding.scope_or(home), home)
|
|
}
|
|
|
|
/// The texts of a command's overrides entry this build cannot use: a key name from a newer
|
|
/// version, a scope this app lacks, a chord this platform reserves.
|
|
fn unusable_texts(&self, i: usize) -> Vec<String> {
|
|
let Some(texts) = self.stored[i].as_deref().and_then(|key| self.overrides.get(key)) else { return Vec::new() };
|
|
let home = &self.catalog[i].scope;
|
|
texts.iter().filter(|t| !Binding::parse(t).is_ok_and(|b| self.usable(&b, home))).cloned().collect()
|
|
}
|
|
|
|
fn set_current(&mut self, i: usize, list: Vec<Bound>) {
|
|
self.unindex(i);
|
|
if let Some(c) = self.current.get_mut(i) {
|
|
*c = list;
|
|
}
|
|
self.index_command(i);
|
|
}
|
|
|
|
fn index_command(&mut self, i: usize) {
|
|
let Some(list) = self.current.get(i) else { return };
|
|
for (k, (t, _)) in list.iter().enumerate() {
|
|
self.by_trigger.entry(t.effective(self.platform)).or_default().insert((i, k));
|
|
}
|
|
}
|
|
|
|
fn unindex(&mut self, i: usize) {
|
|
let Some(list) = self.current.get(i) else { return };
|
|
for (k, (t, _)) in list.iter().enumerate() {
|
|
let t = t.effective(self.platform);
|
|
if let Some(holders) = self.by_trigger.get_mut(&t) {
|
|
holders.remove(&(i, k));
|
|
if holders.is_empty() {
|
|
self.by_trigger.remove(&t);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn rebuild_index(&mut self) {
|
|
self.by_trigger.clear();
|
|
for i in 0..self.current.len() {
|
|
self.index_command(i);
|
|
}
|
|
}
|
|
|
|
/// Recomputes every command's bindings from the defaults and the overrides.
|
|
fn reload(&mut self) {
|
|
self.stored.iter_mut().for_each(|s| *s = None);
|
|
self.current.clone_from(&self.defaults);
|
|
// Exact keys first, so a canonical entry wins over a hand-written variant of it.
|
|
let mut entries: Vec<(&str, &[String])> = self.overrides.iter().collect();
|
|
entries.sort_by_key(|(k, _)| !self.index.contains_key(*k));
|
|
let mut found: Vec<(usize, String, Vec<Bound>)> = Vec::new();
|
|
let mut taken: HashSet<usize> = HashSet::new();
|
|
self.variants = false;
|
|
for (key, texts) in entries {
|
|
let Some(i) = self.resolve_key(key) else { continue };
|
|
self.variants |= !self.index.contains_key(key);
|
|
if !taken.insert(i) {
|
|
continue;
|
|
}
|
|
let home = &self.catalog[i].scope;
|
|
let usable = texts.iter().filter_map(|t| Binding::parse(t).ok()).filter(|b| self.usable(b, home));
|
|
let list = self.normalize(usable, home);
|
|
// An entry none of whose texts is usable here (a key name from a newer version, a scope
|
|
// this app lacks, a chord this platform reserves) keeps the defaults; `[]` means no
|
|
// shortcut.
|
|
let list = if list.is_empty() && !texts.is_empty() { self.defaults[i].clone() } else { list };
|
|
found.push((i, key.to_string(), list));
|
|
}
|
|
for (i, key, list) in found {
|
|
self.stored[i] = Some(key);
|
|
self.current[i] = list;
|
|
}
|
|
self.rebuild_index();
|
|
}
|
|
}
|