using System.ComponentModel;
namespace Terminal.Gui;
// TODO: I don't love the name Shortcut, but I can't think of a better one right now. Shortcut is a bit overloaded.
// TODO: It can mean "Application-scoped key binding" or "A key binding that is displayed in a visual way".
// TODO: I tried `BarItem` but that's not great either as it implies it can only be used in `Bar`s.
///
/// Displays a command, help text, and a key binding. Useful for displaying a command in such as a
/// menu, toolbar, or status bar.
///
///
///
/// When the user clicks on the or presses the key
/// specified by the command is invoked, causing the
/// event to be fired
///
///
/// If is , the
/// be invoked regardless of what View has focus, enabling an application-wide keyboard shortcut.
///
///
/// Set to change the Command text displayed in the .
/// By default, the text is the of .
///
///
/// Set to change the Help text displayed in the .
///
///
/// The text displayed for the is the string representation of the .
/// If the is , the text is not displayed.
///
///
public class Shortcut : View
{
// Hosts the Command, Help, and Key Views. Needed (IIRC - wrote a long time ago) to allow mouse clicks to be handled by the Shortcut.
internal readonly View _container;
///
/// Creates a new instance of .
///
public Shortcut ()
{
CanFocus = true;
Width = Dim.Auto (DimAutoStyle.Content);
Height = Dim.Auto (DimAutoStyle.Content);
//Height = Dim.Auto (minimumContentDim: 1, maximumContentDim: 1);
AddCommand (Gui.Command.HotKey, () => true);
AddCommand (Gui.Command.Accept, OnAccept);
KeyBindings.Add (KeyCode.Space, Gui.Command.Accept);
KeyBindings.Add (KeyCode.Enter, Gui.Command.Accept);
_container = new ()
{
Id = "_container",
// Only the Shortcut (_container) should be able to have focus, not any subviews.
CanFocus = true,
Width = Dim.Auto (DimAutoStyle.Content, 1),
Height = Dim.Auto (DimAutoStyle.Content, 1),
BorderStyle = LineStyle.Dashed
};
CommandView = new ();
HelpView = new ()
{
Id = "_helpView",
// Only the Shortcut should be able to have focus, not any subviews
CanFocus = false,
X = Pos.Align (Alignment.End, AlignmentModes.IgnoreFirstOrLast | AlignmentModes.AddSpaceBetweenItems),
Y = Pos.Center (),
// Helpview is the only subview that doesn't have a min width
Width = Dim.Auto (DimAutoStyle.Text),
Height = Dim.Auto (DimAutoStyle.Text),
ColorScheme = Colors.ColorSchemes ["Error"]
};
_container.Add (HelpView);
// HelpView.TextAlignment = Alignment.End;
HelpView.MouseClick += Shortcut_MouseClick;
KeyView = new ()
{
Id = "_keyView",
// Only the Shortcut should be able to have focus, not any subviews
CanFocus = false,
X = Pos.Align (Alignment.End, AlignmentModes.IgnoreFirstOrLast | AlignmentModes.AddSpaceBetweenItems),
Y = Pos.Center (),
// Bar will set the width of all KeyViews to the width of the widest KeyView.
Width = Dim.Auto (DimAutoStyle.Text),
Height = Dim.Auto (DimAutoStyle.Text),
};
_container.Add (KeyView);
KeyView.MouseClick += Shortcut_MouseClick;
CommandView.Margin.Thickness = new Thickness (1, 0, 1, 0);
HelpView.Margin.Thickness = new Thickness (1, 0, 1, 0);
KeyView.Margin.Thickness = new Thickness (1, 0, 1, 0);
MouseClick += Shortcut_MouseClick;
TitleChanged += Shortcut_TitleChanged;
Initialized += OnInitialized;
Add (_container);
return;
void OnInitialized (object sender, EventArgs e)
{
if (ColorScheme != null)
{
var cs = new ColorScheme (ColorScheme)
{
Normal = ColorScheme.HotNormal,
HotNormal = ColorScheme.Normal
};
KeyView.ColorScheme = cs;
}
}
}
private void Shortcut_MouseClick (object sender, MouseEventEventArgs e)
{
// When the Shortcut is clicked, we want to invoke the Command and Set focus
View view = sender as View;
if (!e.Handled && Command.HasValue)
{
// If the subview (likely CommandView) didn't handle the mouse click, invoke the command.
bool? handled = false;
handled = InvokeCommand (Command.Value);
if (handled.HasValue)
{
e.Handled = handled.Value;
}
}
if (CanFocus)
{
SetFocus ();
}
e.Handled = true;
}
///
public override ColorScheme ColorScheme
{
get
{
if (base.ColorScheme == null)
{
return SuperView?.ColorScheme ?? base.ColorScheme;
}
return base.ColorScheme;
}
set
{
base.ColorScheme = value;
if (ColorScheme != null)
{
var cs = new ColorScheme (ColorScheme)
{
Normal = ColorScheme.HotNormal,
HotNormal = ColorScheme.Normal
};
KeyView.ColorScheme = cs;
}
}
}
#region Command
private Command? _command;
///
/// Gets or sets the that will be invoked when the user clicks on the or
/// presses .
///
public Command? Command
{
get => _command;
set
{
if (value != null)
{
_command = value.Value;
UpdateKeyBinding ();
}
}
}
private View _commandView;
///
/// Gets or sets the View that displays the command text and hotkey.
///
///
///
/// By default, the of the is displayed as the Shortcut's
/// command text.
///
///
/// By default, the CommandView is a with set to
/// .
///
///
/// Setting the will add it to the and remove any existing
/// .
///
///
///
///
/// This example illustrates how to add a to a that toggles the
/// property.
///
///
/// var force16ColorsShortcut = new Shortcut
/// {
/// Key = Key.F6,
/// KeyBindingScope = KeyBindingScope.HotKey,
/// Command = Command.Accept,
/// CommandView = new CheckBox { Text = "Force 16 Colors" }
/// };
/// var cb = force16ColorsShortcut.CommandView as CheckBox;
/// cb.Checked = Application.Force16Colors;
///
/// cb.Toggled += (s, e) =>
/// {
/// var cb = s as CheckBox;
/// Application.Force16Colors = cb!.Checked == true;
/// Application.Refresh();
/// };
/// StatusBar.Add(force16ColorsShortcut);
///
///
public View CommandView
{
get => _commandView;
set
{
if (value == null)
{
throw new ArgumentNullException ();
}
if (_commandView is { })
{
_container.Remove (_commandView);
_commandView?.Dispose ();
}
_commandView = value;
_commandView.Id = "_commandView";
// TODO: Determine if it makes sense to allow the CommandView to be focusable.
// Right now, we don't set CanFocus to false here.
_commandView.CanFocus = false;
// Bar will set the width of all CommandViews to the width of the widest CommandViews.
_commandView.Width = Dim.Auto (DimAutoStyle.Text);
_commandView.Height = Dim.Auto (DimAutoStyle.Text);
_commandView.X = X = Pos.Align (Alignment.End, AlignmentModes.IgnoreFirstOrLast | AlignmentModes.AddSpaceBetweenItems);
_commandView.Y = Pos.Center ();
_commandView.MouseClick += Shortcut_MouseClick;
_commandView.Accept += CommandView_Accept;
_commandView.Margin.Thickness = new (1, 0, 1, 0);
_commandView.HotKeyChanged += (s, e) =>
{
if (e.NewKey != Key.Empty)
{
// Add it
AddKeyBindingsForHotKey (e.OldKey, e.NewKey);
}
};
_commandView.HotKeySpecifier = new ('_');
_container.Remove (HelpView);
_container.Remove (KeyView);
_container.Add (_commandView, HelpView, KeyView);
UpdateKeyBinding();
}
}
private void _commandView_MouseEvent (object sender, MouseEventEventArgs e)
{
e.Handled = true;
}
private void Shortcut_TitleChanged (object sender, StateEventArgs e)
{
// If the Title changes, update the CommandView text. This is a helper to make it easier to set the CommandView text.
// CommandView is public and replaceable, but this is a convenience.
_commandView.Text = Title;
}
private void CommandView_Accept (object sender, CancelEventArgs e)
{
// When the CommandView fires its Accept event, we want to act as though the
// Shortcut was clicked.
var args = new HandledEventArgs ();
Accept?.Invoke (this, args);
if (args.Handled)
{
e.Cancel = args.Handled;
}
}
#endregion Command
#region Help
///
/// The subview that displays the help text for the command. Internal for unit testing.
///
internal View HelpView { get; set; }
///
/// Gets or sets the help text displayed in the middle of the Shortcut.
///
public override string Text
{
get => base.Text;
set
{
//base.Text = value;
if (HelpView != null)
{
HelpView.Text = value;
}
}
}
#endregion Help
#region Key
private Key _key;
///
/// Gets or sets the that will be bound to the command.
///
public Key Key
{
get => _key;
set
{
if (value == null)
{
throw new ArgumentNullException ();
}
_key = value;
if (Command != null)
{
UpdateKeyBinding ();
}
KeyView.Text = $"{Key}";
KeyView.Visible = Key != Key.Empty;
}
}
private KeyBindingScope _keyBindingScope;
///
/// Gets or sets the scope for the key binding for how is bound to .
///
public KeyBindingScope KeyBindingScope
{
get => _keyBindingScope;
set
{
_keyBindingScope = value;
if (Command != null)
{
UpdateKeyBinding ();
}
}
}
///
/// Gets the subview that displays the key. Internal for unit testing.
///
internal View KeyView { get; }
private void UpdateKeyBinding ()
{
if (KeyBindingScope == KeyBindingScope.Application)
{
// return;
}
if (Command != null && Key != null && Key != Key.Empty)
{
// CommandView holds our command/keybinding
// Add a key binding for this command to this Shortcut
if (CommandView.GetSupportedCommands ().Contains (Command.Value))
{
CommandView.KeyBindings.Remove (Key);
CommandView.KeyBindings.Add (Key, KeyBindingScope, Command.Value);
}
else
{
// throw new InvalidOperationException ($"CommandView does not support the command {Command.Value}");
}
}
}
#endregion Key
///
/// The event fired when the command is received. This
/// occurs if the user clicks on the Shortcut or presses .
///
public new event EventHandler Accept;
///
/// Called when the command is received. This
/// occurs if the user clicks on the Bar with the mouse or presses the key bound to
/// Command.Accept (Space by default).
///
protected new bool? OnAccept ()
{
// TODO: This is not completely thought through.
if (Key == null || Key == Key.Empty)
{
return false;
}
var handled = false;
var keyCopy = new Key (Key);
switch (KeyBindingScope)
{
case KeyBindingScope.Application:
// Simulate a key down to invoke the Application scoped key binding
handled = Application.OnKeyDown (keyCopy);
break;
case KeyBindingScope.Focused:
handled = InvokeCommand (Command.Value) == true;
handled = false;
break;
case KeyBindingScope.HotKey:
if (Command.HasValue)
{
//handled = _commandView.InvokeCommand (Gui.Command.HotKey) == true;
//handled = false;
}
break;
}
//if (handled == false)
{
var args = new HandledEventArgs ();
Accept?.Invoke (this, args);
handled = args.Handled;
}
return handled;
}
///
public override bool OnEnter (View view)
{
// TODO: This is a hack. Need to refine this.
var cs = new ColorScheme (ColorScheme)
{
Normal = ColorScheme.Focus,
HotNormal = ColorScheme.HotFocus
};
_container.ColorScheme = cs;
cs = new (ColorScheme)
{
Normal = ColorScheme.HotFocus,
HotNormal = ColorScheme.Focus
};
KeyView.ColorScheme = cs;
return base.OnEnter (view);
}
///
public override bool OnLeave (View view)
{
// TODO: This is a hack. Need to refine this.
var cs = new ColorScheme (ColorScheme)
{
Normal = ColorScheme.Normal,
HotNormal = ColorScheme.HotNormal
};
_container.ColorScheme = cs;
cs = new (ColorScheme)
{
Normal = ColorScheme.HotNormal,
HotNormal = ColorScheme.Normal
};
KeyView.ColorScheme = cs;
return base.OnLeave (view);
}
}