Files
Terminal.Gui/UICatalog/KeyBindingsDialog.cs
Thomas Nind ea7981dc59 Adds Key Binding support. Also refactors Autocomplete and Undo/Redo. (#1556)
* Refactored ProcessKey to use public methods for case logic

* Added KeyBinding class

* Refactored key binding to split key->command from command->implementation

This reduces duplication and simplifies the API

* Finishing key bindings implementation in ListView.

* Adding more unit tests to the ListView.

* Added key bindings to the Button and more features.

* Replaces Action for Func<KeyEvent, bool> on CommandImplementations.

* Allowing commands to have any number of arguments.

* Implementing key bindings on Checkbox view.

* Added test for changing HotKey in Button and made ReplaceKeyBinding protected

* Changed `CommandImplementations` to `Func<KeyEvent, bool>` to better understand current command implementations

* Implementing key bindings in ComboBox.

* Renamed Command keys and fixed ComboBox issues:

- Fixed pressing Esc in ListAndCombos scenario without selecting cause an array out of bounds error
- Changed the Esc key in ComboBox to also collapse the list selection
- Added bool return to public virtual method Expand and Collapse (this is a breaking change)

* Implementing key bindings in DateField.

* Organizing some things.

* Implementing key bindings on TimeField.

* No key bindings on FrameView.

* Added keybinding support to TreeView

* Added mouse support and more features.

* Updating NuGet packages.

* Putting text on the same line.

* Changing function command to Func<bool>.

* Added a read only Position, CursorPosition properties and events.

* Keybindings for GraphView

* Added a stream argument to ApplyEdits to only save the edits.

* Implementing key bindings on the HexView.

* Added MenuOpened event and others bug fixes.

* Fixing typo.

* Unifying constructors initializations.

* Implementing keybindings in the Menu.

* Removing unnecessary variable.

* Implementing keybindings in RadioGroup view.

* Changing Home to TopHome and End to BottomEnd.

* Implementing keybindings in the ScrollView.

* Changing the PageLeft and PageRight keybindings.

* Fixing PageLeft and RightPage.

* Removing CleanUp command.

* Key bindings for TabView

* Keybindings for TableView

* Fixed unit tests for PageDown to correctly assign input focus to the TableView

* Fixes the CalculateLeftColumn method avoiding jump two columns on forward moving.

* Fixes #1525. Gives the same backspace behavior as TextView.

* Changes kill-to-start key to work on Linux too.

* Fixes SelectedStart, SelectedText and some cleaning.

* Implementing keybindings in TextField.

* Updated command names and merged as discussed with @BDisp

- Merged LeftItem and LeftChar to Left (same for Right).
- Also renamed Kill to Cut
- Added ScrollLeft / ScrollRight (and renamed ScrollLineUp to just ScrollUp

* Renamed Command.InsertChar to ToggleOverwriteMode and added Enable/Disable

* Removed 'Mode' suffix from toggle overwrite

* Allows navigation to outside a TextView if IsMdiContainer is true.

* Implementing keybindings in Toplevel.

* Fixing null reference exception.

* Changing to keys instances events instead static.

* Transferring the events to the Toplevel.

* Implementing keybindings in TextView.

* Removing static from the QuitKeyChanged and adding unit test.

* Replacing Added with the Initialized event.

* Ignore control characters and other special keys.

* Changing InvokeKeybindings to return Nullable bool and added two more keys to the Toplevel.

* Implementing keybindings in Autocomplete. I had to derive from View.

* Added keybindings menu item to UICatalog

* Added ClearBinding

* Implementing IAutocomplete, abstract Autocomplete and derived TextViewAutocomplete.

* Implementing keybindings in the TextValidateProvider

* Add keybinding to CellActivationKey.

* Fixing some formats.

* Add ObjectActivationKey to the keybindings.

* Made it much easier to implement abstract base `Autocomplete` in other views by moving methods up out of `TextViewAutocomplete` implementation

* Allowing Autocomplete to popup inside or outside the container.

* Fixes the cursor not being showing if the text length is equal to the view width.

* A unit test to prove the 4df5897.

* Removed unused method `GetCursorPosition` from Autocomplete

* Trimmed down implementation specific methods from IAutocomplete

* Fixed xmldoc comment tag

* Format Autocomplete on multiline and fixes wrap settings.

* Adding keys from a to z to avoid the Key.Space on ToString.

* Fixes the vertical position outside the container.

* Adding more key unit tests.

* Changing comment to upper case and proving that doesn't will breaking nothing.

* Replaces Pos.Bottom to Pos.AnchorEnd.

* Fixes popup on resizing.

* Should only using the Pos.Bottom to position outside the view.

* Fixes #1584

* Fixes https://github.com/migueldeicaza/gui.cs/issues/1584#issuecomment-1027987475

* Fixes some bugs with SelectedItem.

* Command must also return a nullable bool.

* Ensures updating the ComboBox text on leaving the control.

* Only with the nullable bool was possible to make the MoveUp and the MoveDown working.

* Added logging of which scenario failed in test

Co-authored-by: BDisp <bd.bdisp@gmail.com>
2022-02-08 10:40:40 -08:00

200 lines
4.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Terminal.Gui;
namespace UICatalog {
class KeyBindingsDialog : Dialog {
static Dictionary<Command,Key> CurrentBindings = new Dictionary<Command,Key>();
private Command[] commands;
private ListView commandsListView;
private Label keyLabel;
/// <summary>
/// Tracks views as they are created in UICatalog so that their keybindings can
/// be managed.
/// </summary>
private class ViewTracker {
public static ViewTracker Instance;
/// <summary>
/// All views seen so far and a bool to indicate if we have applied keybindings to them
/// </summary>
Dictionary<View, bool> knownViews = new Dictionary<View, bool> ();
private object lockKnownViews = new object ();
private Dictionary<Command, Key> keybindings;
public ViewTracker (View top)
{
RecordView (top);
// Refresh known windows
Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), (m) => {
lock (lockKnownViews) {
RecordView (Application.Top);
ApplyKeyBindingsToAllKnownViews ();
}
return true;
});
}
private void RecordView (View view)
{
if (!knownViews.ContainsKey (view)) {
knownViews.Add (view, false);
}
// may already have subviews that were added to it
// before we got to it
foreach (var sub in view.Subviews) {
RecordView (sub);
}
view.Added += RecordView;
}
internal static void Initialize ()
{
Instance = new ViewTracker (Application.Top);
}
internal void StartUsingNewKeyMap (Dictionary<Command, Key> currentBindings)
{
lock (lockKnownViews) {
// change our knowledge of what keys to bind
this.keybindings = currentBindings;
// Mark that we have not applied the key bindings yet to any views
foreach (var view in knownViews.Keys) {
knownViews [view] = false;
}
}
}
private void ApplyKeyBindingsToAllKnownViews ()
{
if(keybindings == null) {
return;
}
// Key is the view Value is whether we have already done it
foreach (var viewDone in knownViews) {
var view = viewDone.Key;
var done = viewDone.Value;
if (done) {
// we have already applied keybindings to this view
continue;
}
var supported = new HashSet<Command>(view.GetSupportedCommands ());
foreach (var kvp in keybindings) {
// if the view supports the keybinding
if(supported.Contains(kvp.Key))
{
// if the key was bound to any other commands clear that
view.ClearKeybinding (kvp.Key);
view.AddKeyBinding (kvp.Value,kvp.Key);
}
// mark that we have done this view so don't need to set keybindings again on it
knownViews [view] = true;
}
}
}
}
public KeyBindingsDialog () : base("Keybindings", 50,10)
{
if(ViewTracker.Instance == null) {
ViewTracker.Initialize ();
}
// known commands that views can support
commands = Enum.GetValues (typeof (Command)).Cast<Command>().ToArray();
commandsListView = new ListView (commands) {
Width = Dim.Percent (50),
Height = Dim.Percent (100) - 1,
};
commandsListView.SelectedItemChanged += CommandsListView_SelectedItemChanged;
Add (commandsListView);
keyLabel = new Label () {
Text = "Key: None",
Width = Dim.Fill(),
X = Pos.Percent(50),
Y = 0
};
Add (keyLabel);
var btnChange = new Button ("Change") {
X = Pos.Percent (50),
Y = 1,
};
Add (btnChange);
btnChange.Clicked += RemapKey;
var close = new Button ("Ok");
close.Clicked += () => {
Application.RequestStop ();
ViewTracker.Instance.StartUsingNewKeyMap (CurrentBindings);
};
AddButton (close);
var cancel = new Button ("Cancel");
cancel.Clicked += ()=>Application.RequestStop();
AddButton (cancel);
}
private void RemapKey ()
{
var cmd = commands [commandsListView.SelectedItem];
Key? key = null;
// prompt user to hit a key
var dlg = new Dialog ("Enter Key");
dlg.KeyPress += (k) => {
key = k.KeyEvent.Key;
Application.RequestStop ();
};
Application.Run (dlg);
if(key.HasValue) {
CurrentBindings [cmd] = key.Value;
SetTextBoxToShowBinding (cmd);
}
}
private void SetTextBoxToShowBinding (Command cmd)
{
if (CurrentBindings.ContainsKey (cmd)) {
keyLabel.Text = "Key: " + CurrentBindings [cmd].ToString ();
} else {
keyLabel.Text = "Key: None";
}
SetNeedsDisplay ();
}
private void CommandsListView_SelectedItemChanged (ListViewItemEventArgs obj)
{
SetTextBoxToShowBinding ((Command)obj.Value);
}
}
}