// // KeyBindingManager.cs // // Author: Jeffrey Stedfast // // Copyright (C) 2007 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections.Generic; // Terminology: // chord: A 'chord' is a key binding prefix / modifier meant to allow a // wider set of key bindings to exist. If you are familiar with // GNU/Emacs, you might be familiar with the key binding for // 'Save'. This key binding would have a 'chord' of 'Control+x' // and an accel of 'Control+s' - the full binding being: // 'Control+x Control+s'. // // accel: A (possibly partial) key binding for some command/action. // When combined with the mode prefix, results in the full key // binding. // // binding: The full key binding for a command/action. // namespace MonoDevelop.Components.Commands { public class KeyBindingManager : IDisposable { Dictionary> bindings = new Dictionary> (); Dictionary chords = new Dictionary (); List commands = new List (); static KeyBindingManager () { if (isMac) { SelectionModifierAlt = Gdk.ModifierType.Mod5Mask; SelectionModifierControl = Gdk.ModifierType.Mod1Mask | Gdk.ModifierType.MetaMask; SelectionModifierSuper = Gdk.ModifierType.ControlMask; } else { SelectionModifierAlt = Gdk.ModifierType.Mod1Mask; SelectionModifierControl = Gdk.ModifierType.ControlMask; SelectionModifierSuper = Gdk.ModifierType.SuperMask; } } static bool isMac { get { return MonoDevelop.Core.Platform.IsMac; } } public void Dispose () { if (commands == null) return; for (int i = 0; i < commands.Count; i++) commands[i].KeyBindingChanged -= OnKeyBindingChanged; commands = null; } #region Platform-dependent selection modifiers //FIXME: these should be named for what they do, not what they are internal static Gdk.ModifierType SelectionModifierAlt { get; private set; } internal static Gdk.ModifierType SelectionModifierControl { get; private set; } internal static Gdk.ModifierType SelectionModifierSuper { get; private set; } #endregion #region Static Public Helpers For Building Key-Binding Strings static bool KeyIsModifier (Gdk.Key key) { if (key == Gdk.Key.Control_L || key == Gdk.Key.Control_R) return true; else if (key == Gdk.Key.Alt_L || key == Gdk.Key.Alt_R) return true; else if (key == Gdk.Key.Shift_L || key == Gdk.Key.Shift_R) return true; else if (key == Gdk.Key.Meta_L || key == Gdk.Key.Meta_R) return true; else if (key == Gdk.Key.Super_L || key == Gdk.Key.Super_R) return true; return false; } static string ModifierToString (Gdk.ModifierType mod) { string label = string.Empty; if ((mod & Gdk.ModifierType.ControlMask) != 0) label += "Control+"; if ((mod & Gdk.ModifierType.Mod1Mask) != 0) label += "Alt+"; if ((mod & Gdk.ModifierType.ShiftMask) != 0) label += "Shift+"; if ((mod & Gdk.ModifierType.MetaMask) != 0) label += "Meta+"; if ((mod & Gdk.ModifierType.SuperMask) != 0) label += "Super+"; return label; } static string ModifierKeyToString (Gdk.Key key) { string label = string.Empty; if (key == Gdk.Key.Control_L || key == Gdk.Key.Control_R) label += "Control+"; else if (key == Gdk.Key.Alt_L || key == Gdk.Key.Alt_R) label += "Alt+"; else if (key == Gdk.Key.Shift_L || key == Gdk.Key.Shift_R) label += "Shift+"; else if (key == Gdk.Key.Meta_L || key == Gdk.Key.Meta_R) label += "Meta+"; else if (key == Gdk.Key.Super_L || key == Gdk.Key.Super_R) label += "Super+"; return label; } static string KeyToString (Gdk.Key key) { char c = (char) Gdk.Keyval.ToUnicode ((uint) key); if (c != 0) { if (c == ' ') return "Space"; return Char.ToUpper (c).ToString (); } //HACK: Page_Down and Next are synonyms for the same enum value, but alphabetically, Next gets returned // first by enum ToString(). Similarly, Page_Up and Prior are synonyms, but Page_Up is returned. Hence the // default pairing is Next and Page_Up, which is confusingly inconsistent, so we fix this here. // //The same problem applies to some other common keys, so we ensure certain values are mapped // to the most common name. switch (key) { case Gdk.Key.Next: return "Page_Down"; case Gdk.Key.L1: return "F11"; case Gdk.Key.L2: return "F12"; } return key.ToString (); } // This version is used by the CommandManager to track the keystrokes the user has hit for binding lookups. public static KeyboardShortcut[] AccelsFromKey (Gdk.EventKey raw, out bool complete) { KeyboardShortcut[] shortcuts; Gdk.ModifierType modifier; Gdk.Key key; GtkWorkarounds.MapKeys (raw, out key, out modifier, out shortcuts); complete = !KeyIsModifier (key); return shortcuts; } // This version is used by the KeyBindingsPanel to display the keystrokes that the user has hit. public static string AccelLabelFromKey (Gdk.EventKey raw, out bool complete) { KeyboardShortcut[] shortcuts; Gdk.ModifierType modifier; Gdk.Key key; GtkWorkarounds.MapKeys (raw, out key, out modifier, out shortcuts); bool keyIsModifier = KeyIsModifier (key); complete = !keyIsModifier; // The first shortcut is the fully decomposed version string accel = ModifierToString (shortcuts[0].Modifier); if (keyIsModifier) return accel + ModifierKeyToString (shortcuts[0].Key); return accel + KeyToString (shortcuts[0].Key); } public static string AccelLabelFromKey (Gdk.EventKey raw) { bool complete; return AccelLabelFromKey (raw, out complete); } static string AccelLabelFromKey (Gdk.Key key, Gdk.ModifierType modifier, out bool complete) { string accel = ModifierToString (modifier); if ((complete = !KeyIsModifier (key))) return accel + KeyToString (key); return accel + ModifierKeyToString (key); } /// /// Used for canonicalizing bindings after parsing them /// internal static string AccelLabelFromKey (Gdk.Key key, Gdk.ModifierType modifier) { bool complete; return AccelLabelFromKey (key, modifier, out complete); } static bool AccelToKeyPartial (string accel, ref int i, out uint key, out Gdk.ModifierType modifier) { Gdk.ModifierType mod, mask = 0; string str; uint k = 0; int j = i; modifier = Gdk.ModifierType.None; key = 0; if (accel == null || i >= accel.Length) return false; while (i < accel.Length) { for (j = i + 1; j < accel.Length; j++) { if (accel[j] == '+' || accel[j] == '|') break; } str = accel.Substring (i, j - i); if ((mod = ModifierMask (str)) == Gdk.ModifierType.None) { if (str == "Space") k = (uint) Gdk.Key.space; else if (str.Length > 1) k = Gdk.Keyval.FromName (str); else if (str[0] >= 'A' && str[0] <= 'Z') k = (uint) str[0] + 32; else k = (uint) str[0]; break; } mask |= mod; i = j + 1; } i = j; modifier = mask; key = k; return k != 0; } static bool AccelToKeyPartial (string accel, out uint key, out Gdk.ModifierType modifier) { int i = 0; modifier = Gdk.ModifierType.None; key = 0; if (string.IsNullOrEmpty (accel)) return false; if (AccelToKeyPartial (accel, ref i, out key, out modifier) && i == accel.Length) return true; return false; } public static bool AccelToKey (string accel, out uint key, out Gdk.ModifierType modifier) { if (AccelToKeyPartial (accel, out key, out modifier)) return true; modifier = Gdk.ModifierType.None; key = 0; return false; } static bool BindingToKeysPartial (string binding, out uint chordKey, out Gdk.ModifierType chordModifier, out uint key, out Gdk.ModifierType modifier) { int i = 0; chordModifier = Gdk.ModifierType.None; chordKey = 0; modifier = Gdk.ModifierType.None; key = 0; if (string.IsNullOrEmpty (binding)) return false; if (!AccelToKeyPartial (binding, ref i, out key, out modifier)) return false; if (i == binding.Length) { // no mode in this binding, we're done return true; } if (binding[i] != '|') { // bad format return false; } chordModifier = modifier; chordKey = key; i++; return AccelToKeyPartial (binding, ref i, out key, out modifier) && i == binding.Length; } public static bool BindingToKeys (string binding, out uint chordKey, out Gdk.ModifierType chordModifier, out uint key, out Gdk.ModifierType modifier) { if (BindingToKeysPartial (binding, out chordKey, out chordModifier, out key, out modifier)) return true; chordModifier = modifier = Gdk.ModifierType.None; chordKey = key = 0; return false; } public static string Binding (string chord, string accel) { if (string.IsNullOrEmpty (chord)) { if (string.IsNullOrEmpty (accel)) return null; return accel; } if (string.IsNullOrEmpty (accel)) return chord; return chord + "|" + accel; } #endregion #region Display label formatting public static string BindingToDisplayLabel (string binding, bool concise) { return BindingToDisplayLabel (binding, concise, false); } public static string BindingToDisplayLabel (string binding, bool concise, bool includeIncomplete) { Gdk.ModifierType chordMod, mod; string label = string.Empty; uint chordKey, key; if (includeIncomplete) { BindingToKeysPartial (binding, out chordKey, out chordMod, out key, out mod); } else if (!BindingToKeys (binding, out chordKey, out chordMod, out key, out mod)) { return null; } if (chordKey != 0) label += ModifierToDisplayLabel (chordMod, concise) + KeyToDisplayLabel ((Gdk.Key) chordKey) + (isMac ? " " : "|"); label += ModifierToDisplayLabel (mod, concise); if (key != 0) label += KeyToDisplayLabel ((Gdk.Key) key); return label; } public static string BindingToDisplayLabel (KeyBinding binding, bool concise) { string label; if (!binding.Chord.IsEmpty) label = ModifierToDisplayLabel (binding.Chord.Modifier, concise) + KeyToDisplayLabel (binding.Chord.Key) + (isMac ? " " : "|"); else label = string.Empty; return label + ModifierToDisplayLabel (binding.Accel.Modifier, concise) + KeyToDisplayLabel (binding.Accel.Key); } static string KeyToDisplayLabel (Gdk.Key key) { if (isMac) { char appl = AppleMapKeyToSymbol (key); if (appl != '\0') { return new string (appl, 1); } } switch (key) { case Gdk.Key.Page_Down: //Gdk.Key.Next: return "Page Down"; case Gdk.Key.Page_Up: //case Gdk.Key.Prior: return "Page Up"; } return KeyToString (key); } static string ModifierToDisplayLabel (Gdk.ModifierType mod, bool concise) { if (isMac) return AppleMapModifierToSymbols (mod); string label = string.Empty; if ((mod & Gdk.ModifierType.ControlMask) != 0) label += concise? "Ctrl+" : "Control+"; if ((mod & Gdk.ModifierType.Mod1Mask) != 0) label += "Alt+"; if ((mod & Gdk.ModifierType.ShiftMask) != 0) label += "Shift+"; if ((mod & Gdk.ModifierType.MetaMask) != 0) label += "Meta+"; if ((mod & Gdk.ModifierType.SuperMask) != 0) label += "Super+"; return label; } static Gdk.ModifierType ModifierMask (string name) { switch (name) { case "Alt": return Gdk.ModifierType.Mod1Mask; case "Shift": return Gdk.ModifierType.ShiftMask; case "Control": case "Ctrl": return Gdk.ModifierType.ControlMask; case "Meta": return Gdk.ModifierType.MetaMask; case "Super": return Gdk.ModifierType.SuperMask; default: return Gdk.ModifierType.None; } } /// /// Converts the old shortcut format into the new "key binding" format /// static string ShortcutToBinding (string shortcut) { Gdk.ModifierType mod, mask = 0; int i = 0, j; Gdk.Key key; string str; if (string.IsNullOrEmpty (shortcut)) return null; while (i < shortcut.Length) { if ((j = shortcut.IndexOf ('|', i + 1)) == -1) { // we are at the last part of the shortcut, the Gdk.Key j = shortcut.Length; } str = shortcut.Substring (i, j - i); if ((mod = ModifierMask (str)) == Gdk.ModifierType.None) { if (str.Length > 1) { if (!Gdk.Key.TryParse (str, out key)) { Console.WriteLine ("WARNING: invalid Gdk.Key portion of shortcut {0}", shortcut); return null; } } else if (str[0] >= 'a' && str[0] <= 'z') key = (Gdk.Key) str[0] - 32; else key = (Gdk.Key) str[0]; if (j < shortcut.Length) Console.WriteLine ("WARNING: trailing data after Gdk.Key portion of shortcut {0}", shortcut); return AccelLabelFromKey (key, mask); } mask |= mod; i = j + 1; } Console.WriteLine ("WARNING: Incomplete shortcut '{0}'?", shortcut); return null; } /// /// Returns true if @shortcut is in the old string format or false if it is the newer string format. /// static bool IsShortcutFormat (string shortcut) { int i = 0; if (string.IsNullOrEmpty (shortcut)) return false; if (shortcut.Length == 1 && (shortcut[0] == '|' || shortcut[0] == '+')) { // single-key binding return false; } while (i < shortcut.Length && (shortcut[i] != '|' && shortcut[i] != '+')) i++; return i < shortcut.Length && shortcut[i] == '|' && ModifierMask (shortcut.Substring (0, i)) != Gdk.ModifierType.None; } public static string CanonicalizeBinding (string binding) { Gdk.ModifierType chordMod, mod; string accel, chord = null; uint chordKey, key; if (string.IsNullOrEmpty (binding)) return null; if (IsShortcutFormat (binding)) return ShortcutToBinding (binding); if (!BindingToKeys (binding, out chordKey, out chordMod, out key, out mod)) { Console.WriteLine ("WARNING: failed to canonicalize binding {0}", binding); return null; } if (chordKey != 0) chord = AccelLabelFromKey ((Gdk.Key) chordKey, chordMod); accel = AccelLabelFromKey ((Gdk.Key) key, mod); return Binding (chord, accel); } internal static string FixChordSeparators (string binding) { // converts old style '|' separators to new scheme with chord detection. // Examples: // Control|X -> Control+X // Alt|| -> Alt+| // Alt|+ -> Alt++ // this conversion is required for proper key detection, otherwise // keys bindings like Control|X and Control+X will not be detected as // eqal / duplicate bindings. if (string.IsNullOrEmpty (binding)) return binding; var chars = binding.ToCharArray (); bool foundChordSep = false; // first and last characters are never separators, skip them for (int i = 1; i < binding.Length - 1; i++) { // chords can not start with a '+', therefore all bindings // whre a '+' occurs before a '|' have already the right style if (chars [i] == '+' && !foundChordSep) return binding; // make sure we don't convert the real '|' key if (chars [i] == '|' && chars [i - 1] != '|') { foundChordSep = true; chars [i] = '+'; } } var result = new string (chars); return result; } #endregion #region Public Methods public bool BindingExists (KeyBinding binding) { return bindings.ContainsKey (binding); } public bool ChordExists (KeyboardShortcut chord) { return chords.ContainsKey (chord); } public bool AnyChordExists (KeyboardShortcut[] chords) { foreach (var chord in chords) { if (ChordExists (chord)) return true; } return false; } static bool ChordChanged (KeyBinding old, KeyBinding @new) { if (old == null && @new == null) return false; if (old == null || @new == null) return true; return !old.Chord.Equals (@new.Chord); } void SetBinding (Command command, KeyBinding oldKeyBinding, KeyBinding newKeyBinding) { List list; int refs; if (ChordChanged (oldKeyBinding, newKeyBinding)) { // keep track of valid modes if (oldKeyBinding != null && !oldKeyBinding.Chord.IsEmpty) { if ((refs = chords[oldKeyBinding.Chord] - 1) == 0) chords.Remove (oldKeyBinding.Chord); else chords[oldKeyBinding.Chord] = refs; } if (newKeyBinding != null && !newKeyBinding.Chord.IsEmpty) { if (!chords.ContainsKey (newKeyBinding.Chord)) chords.Add (newKeyBinding.Chord, 1); else chords[newKeyBinding.Chord]++; } } if (oldKeyBinding != null) { list = bindings[oldKeyBinding]; list.Remove (command); if (list.Count == 0) bindings.Remove (oldKeyBinding); } if (newKeyBinding != null) { if (!bindings.ContainsKey (newKeyBinding)) { list = new List (); list.Add (command); bindings.Add (newKeyBinding, list); } else { list = bindings[newKeyBinding]; list.Add (command); } } } void OnKeyBindingChanged (object o, KeyBindingChangedEventArgs args) { SetBinding (args.Command, args.OldKeyBinding, args.NewKeyBinding); } void OnAlternateKeyBindingChanged (object sender, AlternateKeyBindingChangedEventArgs args) { var upperBound = Math.Max (args.NewKeyBinding != null ? args.NewKeyBinding.Length : 0, args.OldKeyBinding != null ? args.OldKeyBinding.Length : 0); for (int i = 0; i < upperBound; i++) { SetBinding (args.Command, args.OldKeyBinding != null && i < args.OldKeyBinding.Length ? args.OldKeyBinding[i] : null, args.NewKeyBinding != null && i < args.NewKeyBinding.Length ? args.NewKeyBinding[i] : null ); } } public bool CommandIsRegistered (Command command) { return commands.Contains (command); } public void RegisterCommand (Command command) { if (commands.Contains (command)) { Console.WriteLine ("WARNING: trying to re-register command {0}", command); return; } SetBinding (command, null, command.KeyBinding); foreach (var binding in command.AlternateKeyBindings) { SetBinding (command, null, binding); } command.KeyBindingChanged += OnKeyBindingChanged; command.AlternateKeyBindingChanged += OnAlternateKeyBindingChanged; commands.Add (command); } public void UnregisterCommand (Command command) { if (!commands.Contains (command)) { Console.WriteLine ("WARNING: trying to unregister unknown command {0}", command); return; } command.KeyBindingChanged -= OnKeyBindingChanged; command.AlternateKeyBindingChanged -= OnAlternateKeyBindingChanged; commands.Remove (command); RemoveKeyBinding (command, command.KeyBinding); foreach (var altBinding in command.AlternateKeyBindings) { RemoveKeyBinding (command, altBinding); } } void RemoveKeyBinding (Command command, KeyBinding bindingToRemove) { if (bindingToRemove == null) return; var list = bindings [bindingToRemove]; int refs; list.Remove (command); if (list.Count == 0) bindings.Remove (bindingToRemove); if (!bindingToRemove.Chord.IsEmpty && chords.ContainsKey (bindingToRemove.Chord)) { if ((refs = chords [bindingToRemove.Chord] - 1) == 0) chords.Remove (bindingToRemove.Chord); else chords [bindingToRemove.Chord] = refs; } } /// /// Returns the list of commands registered for the specified key binding. /// public List Commands (KeyBinding binding) { if (binding == null || !BindingExists (binding)) return null; return bindings[binding]; } #endregion #region Apple symbol mappings static string AppleMapModifierToSymbols (Gdk.ModifierType mod) { string ret = ""; if ((mod & Gdk.ModifierType.ControlMask) != 0) { ret += "⌃"; mod ^= Gdk.ModifierType.ControlMask; } if ((mod & Gdk.ModifierType.Mod1Mask) != 0) { ret += "⌥"; mod ^= Gdk.ModifierType.Mod1Mask; } if ((mod & Gdk.ModifierType.ShiftMask) != 0) { ret += "⇧"; mod ^= Gdk.ModifierType.ShiftMask; } if ((mod & Gdk.ModifierType.MetaMask) != 0) { ret += "⌘"; mod ^= Gdk.ModifierType.MetaMask; } if (mod != 0) throw new InvalidOperationException ("Unexpected modifiers: " + mod.ToString ()); return ret; } static char AppleMapKeyToSymbol (Gdk.Key key) { // unicode codes from http://macbiblioblog.blogspot.com/2005/05/special-key-symbols.html // unmapped: // ⇤ tab back // ⌤ enter switch (key) { case Gdk.Key.Escape: return '⎋'; case Gdk.Key.Tab: return '⇥'; case Gdk.Key.Caps_Lock: return '⇪'; case Gdk.Key.Shift_L: case Gdk.Key.Shift_R: return '⇧'; case Gdk.Key.Control_L: case Gdk.Key.Control_R: return '⌃'; case Gdk.Key.BackSpace: return '⌫'; case Gdk.Key.Delete: return '⌦'; case Gdk.Key.Home: return '\u2196'; // ↖ //return '\u21F1'; // ⇱ //return '⇱'; case Gdk.Key.End: return '\u2198'; // ↘ //return '\u21F2'; // ⇲ //return '⇲'; case Gdk.Key.Page_Up: return '⇞'; case Gdk.Key.Page_Down: return '⇟'; case Gdk.Key.Up: return '↑'; case Gdk.Key.Down: return '↓'; case Gdk.Key.Left: return '←'; case Gdk.Key.Right: return '→'; case Gdk.Key.Clear: return '⌧'; case Gdk.Key.Num_Lock: return '⇭'; case Gdk.Key.Return: return '⏎'; case Gdk.Key.space: return '␣'; case Gdk.Key.Meta_L: case Gdk.Key.Meta_R: return '⌘'; case Gdk.Key.Alt_L: case Gdk.Key.Alt_R: return '⌥'; } return (char)0; } /* static Gdk.Key AppleMapSymbolToKey (char ch) { // unicode codes from http://macbiblioblog.blogspot.com/2005/05/special-key-symbols.html // unmapped: // ⇤ tab back // ⌥ option (alt, alternative) // ⌘ command // ⌤ enter switch (ch) { case '⎋': return Gdk.Key.Escape; case '⇥': return Gdk.Key.Tab; case '⇪': return Gdk.Key.Caps_Lock; case '⇧': return Gdk.Key.Shift_L; case '⌃': return Gdk.Key.Control_L; case '⌫': return Gdk.Key.BackSpace; case '⌦': return Gdk.Key.Delete; case '⇱': return Gdk.Key.Home; case '⇲': return Gdk.Key.End; case '⇞': return Gdk.Key.Page_Up; case '⇟': return Gdk.Key.Page_Down; case '↑': return Gdk.Key.Up; case '↓': return Gdk.Key.Down; case '←': return Gdk.Key.Left; case '→': return Gdk.Key.Right; case '⌧': return Gdk.Key.Clear; case '⇭': return Gdk.Key.Num_Lock; case '⏎': return Gdk.Key.Return; case '␣': return Gdk.Key.space; case '⌘': return Gdk.Key.Meta_L; case '⌥': return Gdk.Key.Alt_L; } return (Gdk.Key)0; } */ #endregion } public class KeyBinding : IEquatable { public KeyBinding (KeyboardShortcut chord, KeyboardShortcut accel) { Chord = chord; Accel = accel; } public KeyBinding (KeyboardShortcut accel) { Chord = KeyboardShortcut.Empty; Accel = accel; } public static bool TryParse (string str, out KeyBinding binding) { Gdk.ModifierType chordModifier; Gdk.ModifierType modifier; uint chordKey; uint key; if (!KeyBindingManager.BindingToKeys (str, out chordKey, out chordModifier, out key, out modifier)) { binding = null; return false; } KeyboardShortcut chord = new KeyboardShortcut ((Gdk.Key) chordKey, chordModifier); KeyboardShortcut accel = new KeyboardShortcut ((Gdk.Key) key, modifier); binding = new KeyBinding (chord, accel); return true; } public KeyboardShortcut Chord { get; private set; } public KeyboardShortcut Accel { get; private set; } public override int GetHashCode () { return Chord.GetHashCode () ^ Accel.GetHashCode (); } public override bool Equals (object obj) { return obj is KeyBinding && Equals ((KeyBinding) obj); } #region IEquatable[KeyBinding] implementation public bool Equals (KeyBinding other) { return Chord.Equals (other.Chord) && Accel.Equals (other.Accel); } #endregion public override string ToString () { string chord; if (!Chord.IsEmpty) chord = KeyBindingManager.AccelLabelFromKey (Chord.Key, Chord.Modifier) + "|"; else chord = string.Empty; return chord + KeyBindingManager.AccelLabelFromKey (Accel.Key, Accel.Modifier); } } //#region KeyBindingException // public class KeyBindingConflictException : Exception { // Command command; // string binding; // // public KeyBindingConflictException (Command command, string binding) // : base (String.Format ("An existing command, {0}, is already bound to {1}", // command, binding)) // { // this.command = command; // this.binding = binding; // } // // public Command Command { // get { return command; } // } // // public string Binding { // get { return binding; } // } // } //#endregion }