Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #3778: Decouples Command from KeyBindings #3880

Merged
merged 42 commits into from
Dec 10, 2024

Conversation

tig
Copy link
Collaborator

@tig tig commented Dec 5, 2024

Fixes

Proposed Changes/Todos

  • Change CommandContext to be generic
  • Refactor KeyBindings to use new CommandContext<T>
  • Implement just enough mouse binding to ensure design will work.
  • Combine KeyBindings, ApplicationKeyBindings, and MouseBindings to reduce duplicate code.
  • Update Conceptual Docs

Pull Request checklist:

  • I've named my PR in the form of "Fixes #issue. Terse description."
  • My code follows the style guidelines of Terminal.Gui - if you use Visual Studio, hit CTRL-K-D to automatically reformat your files before committing.
  • My code follows the Terminal.Gui library design guidelines
  • I ran dotnet test before commit
  • I have made corresponding changes to the API documentation (using /// style comments)
  • My changes generate no new warnings
  • I have checked my code and corrected any poor grammar or misspellings
  • I conducted basic QA to assure all features are working

@tig
Copy link
Collaborator Author

tig commented Dec 7, 2024

With what I've done so far all of the Keyboard and Mouse handling code in CharMap is reduced to this:

       AddCommand (Command.Up, commandContext => Move (commandContext, -16));
       AddCommand (Command.Down, commandContext => Move (commandContext, 16));
       AddCommand (Command.Left, commandContext => Move (commandContext, -1));
       AddCommand (Command.Right, commandContext => Move (commandContext, 1));

       AddCommand (Command.PageUp, commandContext => Move (commandContext, -(Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16));
       AddCommand (Command.PageDown, commandContext => Move (commandContext, (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16));
       AddCommand (Command.Start, commandContext => Move (commandContext, -SelectedCodePoint));
       AddCommand (Command.End, commandContext => Move (commandContext, MAX_CODE_POINT - SelectedCodePoint));

       AddCommand (Command.ScrollDown, () => ScrollVertical (1));
       AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
       AddCommand (Command.ScrollRight, () => ScrollHorizontal (1));
       AddCommand (Command.ScrollLeft, () => ScrollHorizontal (-1));

       AddCommand (Command.Accept, HandleAcceptCommand);
       AddCommand (Command.Select, HandleSelectCommand);
       AddCommand (Command.Context, HandleContextCommand);

       KeyBindings.Add (Key.CursorUp, Command.Up);
       KeyBindings.Add (Key.CursorDown, Command.Down);
       KeyBindings.Add (Key.CursorLeft, Command.Left);
       KeyBindings.Add (Key.CursorRight, Command.Right);
       KeyBindings.Add (Key.PageUp, Command.PageUp);
       KeyBindings.Add (Key.PageDown, Command.PageDown);
       KeyBindings.Add (Key.Home, Command.Start);
       KeyBindings.Add (Key.End, Command.End);
       KeyBindings.Add (ContextMenu.DefaultKey, Command.Context);

       MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept);
       MouseBindings.ReplaceCommands(MouseFlags.Button3Clicked, Command.Context);
       MouseBindings.ReplaceCommands (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Context);
       MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
       MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp);
       MouseBindings.Add (MouseFlags.WheeledLeft, Command.ScrollLeft);
       MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight);

@tig tig requested review from tznind, dodexahedron and BDisp and removed request for BDisp and tznind December 9, 2024 02:30
@tig
Copy link
Collaborator Author

tig commented Dec 9, 2024

Here's the excerpt from keyboard.md highlighting the new model found in this PR:

Keyboard APIs

Terminal.Gui provides the following APIs for handling keyboard input:

  • Key - @Terminal.Gui.Key provides a platform-independent abstraction for common keyboard operations. It is used for processing keyboard input and raising keyboard events. This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class instead of the low-level KeyCode enum when possible.
  • Key Bindings - Key Bindings provide a declarative method for handling keyboard input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.KeyBindings to indicate which key presses will invoke the command.
  • Key Events - The Key Bindings API is rich enough to support the vast majority of use-cases. However, in some cases subscribing directly to key events is needed (e.g. when capturing arbitrary typing by a user). Use @Terminal.Gui.View.KeyDown and related events in these cases.

Each of these APIs are described more fully below.

Key Bindings

Key Bindings is the preferred way of handling keyboard input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.KeyBindings to indicate which key presses will invoke the command. For example, if a View wants to respond to the user pressing the up arrow key to scroll up it would do this

public MyView : View
{
  AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
  KeyBindings.Add (Key.CursorUp, Command.ScrollUp);
}

The Character Map Scenario includes a View called CharMap that is a good example of the Key Bindings API.

The Command enum lists generic operations that are implemented by views. For example Command.Accept in a Button results in the Accepting event
firing while in TableView it is bound to CellActivated. Not all commands
are implemented by all views (e.g. you cannot scroll in a Button). Use the @Terminal.Gui.View.GetSupportedCommands method to determine which commands are implemented by a View.

The default key for activating a button is Space. You can change this using
KeyBindings.ReplaceKey():

var btn = new Button () { Title = "Press me" };
btn.KeyBindings.ReplaceKey (btn.KeyBindings.GetKeyFromCommands (Command.Accept));

Key Bindings can be added at the Application or View level.

For Application-scoped Key Bindings there are two categories of Application-scoped Key Bindings:

  1. Application Command Key Bindings - Bindings for Commands supported by @Terminal.Gui.Application. For example, @Terminal.Gui.Application.QuitKey, which is bound to Command.Quit and results in @Terminal.Gui.Application.RequestStop being called.
  2. Application Key Bindings - Bindings for Commands supported on arbitrary Views that are meant to be invoked regardless of which part of the application is visible/active.

Use @Terminal.Gui.Application.KeyBindings to add or modify Application-scoped Key Bindings.

View-scoped Key Bindings also have two categories:

  1. HotKey Bindings - These bind to Commands that will be invoked regardless of whether the View has focus or not. The most common use-case for HotKey bindings is @Terminal.Gui.View.HotKey. For example, a Button with a Title of _OK, the user can press Alt-O and the button will be accepted regardless of whether it has focus or not. Add and modify HotKey bindings with @Terminal.Gui.View.HotKeyBindings.
  2. Focused Bindings - These bind to Commands that will be invoked only when the View has focus. Focused Key Bindings are the easiest way to enable a View to support responding to key events. Add and modify Focused bindings with @Terminal.Gui.View.KeyBindings.

Application-Scoped Key Bindings

HotKey

A HotKey is a key press that selects a visible UI item. For selecting items across Views (e.g. a Button in a Dialog) the key press must have the Alt modifier. For selecting items within a View that are not Views themselves, the key press can be key without the Alt modifier. For example, in a Dialog, a Button with the text of "_Text" can be selected with Alt+T. Or, in a Menu with "_File _Edit", Alt+F will select (show) the "_File" menu. If the "_File" menu has a sub-menu of "_New" Alt+N or N will ONLY select the "_New" sub-menu if the "_File" menu is already opened.

By default, the Text of a View is used to determine the HotKey by looking for the first occurrence of the @Terminal.Gui.View.HotKeySpecifier (which is underscore (_) by default). The character following the underscore is the HotKey. If the HotKeySpecifier is not found in Text, the first character of Text is used as the HotKey. The Text of a View can be changed at runtime, and the HotKey will be updated accordingly. @"Terminal.Gui.View.HotKey" is virtual enabling this behavior to be customized.

Shortcut

A Shortcut is an opinionated (visually & API) View for displaying a command, help text, key key press that invokes a Command.

The Command can be invoked even if the View that defines them is not focused or visible (but the View must be enabled). Shortcuts can be any key press; Key.A, Key.A.WithCtrl, Key.A.WithCtrl.WithAlt, Key.Del, and Key.F1, are all valid.

Shortcuts are used to define application-wide actions or actions that are not visible (e.g. Copy).

MenuBar, ContextMenu, and StatusBar support Shortcuts.

@tig
Copy link
Collaborator Author

tig commented Dec 9, 2024

And here's the new mouse.md:

Mouse APIs

Terminal.Gui provides the following APIs for handling mouse input:

  • MouseEventArgs - @Terminal.Gui.MouseEventArgs provides a platform-independent abstraction for common mouse operations. It is used for processing mouse input and raising mouse events.
  • Mouse Bindings - Mouse Bindings provide a declarative method for handling mouse input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.MouseBindings to indicate which mouse events will invoke the command.
  • Mouse Events - The Mouse Bindings API is rich enough to support the majority of use-cases. However, in some cases subscribing directly to key events is needed (e.g. drag & drop). Use @Terminal.Gui.View.MouseEvent and related events in these cases.

Each of these APIs are described more fully below.

Mouse Bindings

Mouse Bindings is the preferred way of handling mouse input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.MouseBindings to indicate which mouse events will invoke the command. For example, if a View wants to respond to the user using the mouse wheel to scroll up, it would do this:

public MyView : View
{
  AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
  MouseBindings.Add (MouseFlags.Button1DoubleClick, Command.ScrollUp);
}

The Command enum lists generic operations that are implemented by views.

@tig
Copy link
Collaborator Author

tig commented Dec 9, 2024

@tznind, i'd love your comments on all this.

@tig tig requested a review from tznind December 9, 2024 02:35
Copy link
Collaborator

@BDisp BDisp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lovely work.

@tig
Copy link
Collaborator Author

tig commented Dec 10, 2024

@tznind thanks a ton! As I had hoped, you nailed it.

So. Much. Better.

@tig tig requested a review from tznind December 10, 2024 06:20
@tig tig merged commit 7676f89 into gui-cs:v2_develop Dec 10, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Command is too tightly coupled with KeyBindings
3 participants