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; } }