namespace Terminal.Gui;
///
/// objects are contained by s. Each
/// has a title, a shortcut (hotkey), and an that will be invoked when
/// the is pressed. The will be a global hotkey for
/// the application in the current context of the screen. The color of the will be
/// changed after each ~. A set to `~F1~ Help` will render as *F1* using
/// and *Help* as .
///
public class StatusItem
{
/// Initializes a new .
/// Shortcut to activate the .
/// Title for the .
/// Action to invoke when the is activated.
/// Function to determine if the action can currently be executed.
public StatusItem (Key shortcut, string title, Action action, Func canExecute = null)
{
Title = title ?? "";
Shortcut = shortcut;
Action = action;
CanExecute = canExecute;
}
/// Gets or sets the action to be invoked when the statusbar item is triggered
/// Action to invoke.
public Action Action { get; set; }
///
/// Gets or sets the action to be invoked to determine if the can be triggered. If
/// returns the status item will be enabled. Otherwise, it will be
/// disabled.
///
/// Function to determine if the action is can be executed or not.
public Func CanExecute { get; set; }
/// Gets or sets arbitrary data for the status item.
/// This property is not used internally.
public object Data { get; set; }
/// Gets the global shortcut to invoke the action on the menu.
public Key Shortcut { get; set; }
/// Gets or sets the title.
/// The title.
///
/// The colour of the will be changed after each ~. A
/// set to `~F1~ Help` will render as *F1* using and
/// *Help* as .
///
public string Title { get; set; }
///
/// Returns if the status item is enabled. This method is a wrapper around
/// .
///
public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
}
///
/// A status bar is a that snaps to the bottom of a displaying set of
/// s. The should be context sensitive. This means, if the main menu
/// and an open text editor are visible, the items probably shown will be ~F1~ Help ~F2~ Save ~F3~ Load. While a dialog
/// to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. So for each context must be a
/// new instance of a status bar.
///
public class StatusBar : View
{
private static Rune _shortcutDelimiter = (Rune)'=';
private StatusItem [] _items = { };
private StatusItem _itemToInvoke;
/// Initializes a new instance of the class.
public StatusBar () : this (new StatusItem [] { }) { }
///
/// Initializes a new instance of the class with the specified set of
/// s. The will be drawn on the lowest line of the terminal or
/// (if not null).
///
/// A list of status bar items.
public StatusBar (StatusItem [] items)
{
if (items is { })
{
Items = items;
}
CanFocus = false;
ColorScheme = Colors.ColorSchemes ["Menu"];
X = 0;
Y = Pos.AnchorEnd (1);
Width = Dim.Fill ();
Height = 1;
AddCommand (Command.Accept, InvokeItem);
}
/// The items that compose the
public StatusItem [] Items
{
get => _items;
set
{
foreach (StatusItem item in _items)
{
KeyBindings.Remove (item.Shortcut);
}
_items = value;
foreach (StatusItem item in _items)
{
KeyBindings.Add (item.Shortcut, KeyBindingScope.HotKey, Command.Accept);
}
}
}
/// Gets or sets shortcut delimiter separator. The default is "-".
public static Rune ShortcutDelimiter
{
get => _shortcutDelimiter;
set
{
if (_shortcutDelimiter != value)
{
_shortcutDelimiter = value == default (Rune) ? (Rune)'=' : value;
}
}
}
/// Inserts a in the specified index of .
/// The zero-based index at which item should be inserted.
/// The item to insert.
public void AddItemAt (int index, StatusItem item)
{
List itemsList = new (Items);
itemsList.Insert (index, item);
Items = itemsList.ToArray ();
SetNeedsDisplay ();
}
///
protected internal override bool OnMouseEvent (MouseEvent me)
{
if (me.Flags != MouseFlags.Button1Clicked)
{
return false;
}
var pos = 1;
for (var i = 0; i < Items.Length; i++)
{
if (me.X >= pos && me.X < pos + GetItemTitleLength (Items [i].Title))
{
StatusItem item = Items [i];
if (item.IsEnabled ())
{
Run (item.Action);
}
break;
}
pos += GetItemTitleLength (Items [i].Title) + 3;
}
return true;
}
///
public override void OnDrawContent (Rectangle viewport)
{
Move (0, 0);
Driver.SetAttribute (GetNormalColor ());
for (var i = 0; i < Frame.Width; i++)
{
Driver.AddRune ((Rune)' ');
}
Move (1, 0);
Attribute scheme = GetNormalColor ();
Driver.SetAttribute (scheme);
for (var i = 0; i < Items.Length; i++)
{
string title = Items [i].Title;
Driver.SetAttribute (DetermineColorSchemeFor (Items [i]));
for (var n = 0; n < Items [i].Title.GetRuneCount (); n++)
{
if (title [n] == '~')
{
if (Items [i].IsEnabled ())
{
scheme = ToggleScheme (scheme);
}
continue;
}
Driver.AddRune ((Rune)title [n]);
}
if (i + 1 < Items.Length)
{
Driver.AddRune ((Rune)' ');
Driver.AddRune (Glyphs.VLine);
Driver.AddRune ((Rune)' ');
}
}
}
///
public override bool OnEnter (View view)
{
Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
return base.OnEnter (view);
}
///
public override bool? OnInvokingKeyBindings (Key keyEvent)
{
// This is a bit of a hack. We want to handle the key bindings for status bar but
// InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
// So before we call the base class we set SelectedItem appropriately.
Key key = new (keyEvent);
if (KeyBindings.TryGet (key, out _))
{
// Search RadioLabels
foreach (StatusItem item in Items)
{
if (item.Shortcut == key)
{
_itemToInvoke = item;
//keyEvent.Scope = KeyBindingScope.HotKey;
break;
}
}
}
return base.OnInvokingKeyBindings (keyEvent);
}
/// Removes a at specified index of .
/// The zero-based index of the item to remove.
/// The removed.
public StatusItem RemoveItem (int index)
{
List itemsList = new (Items);
StatusItem item = itemsList [index];
itemsList.RemoveAt (index);
Items = itemsList.ToArray ();
SetNeedsDisplay ();
return item;
}
private Attribute DetermineColorSchemeFor (StatusItem item)
{
if (item is { })
{
if (item.IsEnabled ())
{
return GetNormalColor ();
}
return ColorScheme.Disabled;
}
return GetNormalColor ();
}
private int GetItemTitleLength (string title)
{
var len = 0;
foreach (char ch in title)
{
if (ch == '~')
{
continue;
}
len++;
}
return len;
}
private bool? InvokeItem ()
{
if (_itemToInvoke is { Action: { } })
{
_itemToInvoke.Action.Invoke ();
return true;
}
return false;
}
private void Run (Action action)
{
if (action is null)
{
return;
}
Application.MainLoop.AddIdle (
() =>
{
action ();
return false;
}
);
}
private Attribute ToggleScheme (Attribute scheme)
{
Attribute result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal;
Driver.SetAttribute (result);
return result;
}
}