Merge branch 'v2_develop' into v2_2491-Arrangement-Overlapped

This commit is contained in:
Tig
2024-08-13 15:59:06 -06:00
78 changed files with 2359 additions and 1095 deletions

View File

@@ -9,7 +9,6 @@ public static partial class Application // Keyboard handling
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
[JsonConverter (typeof (KeyJsonConverter))]
public static Key NextTabKey
{
get => _nextTabKey;
@@ -27,7 +26,6 @@ public static partial class Application // Keyboard handling
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
[JsonConverter (typeof (KeyJsonConverter))]
public static Key PrevTabKey
{
get => _prevTabKey;
@@ -45,7 +43,6 @@ public static partial class Application // Keyboard handling
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
[JsonConverter (typeof (KeyJsonConverter))]
public static Key NextTabGroupKey
{
get => _nextTabGroupKey;
@@ -63,7 +60,6 @@ public static partial class Application // Keyboard handling
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
[JsonConverter (typeof (KeyJsonConverter))]
public static Key PrevTabGroupKey
{
get => _prevTabGroupKey;
@@ -81,7 +77,6 @@ public static partial class Application // Keyboard handling
/// <summary>Gets or sets the key to quit the application.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
[JsonConverter (typeof (KeyJsonConverter))]
public static Key QuitKey
{
get => _quitKey;

View File

@@ -7,17 +7,17 @@ namespace Terminal.Gui;
/// </summary>
[JsonSerializable (typeof (Attribute))]
[JsonSerializable (typeof (Color))]
[JsonSerializable (typeof (ThemeScope))]
[JsonSerializable (typeof (ColorScheme))]
[JsonSerializable (typeof (SettingsScope))]
[JsonSerializable (typeof (AppScope))]
[JsonSerializable (typeof (SettingsScope))]
[JsonSerializable (typeof (Key))]
[JsonSerializable (typeof (GlyphDefinitions))]
[JsonSerializable (typeof (ConfigProperty))]
[JsonSerializable (typeof (Alignment))]
[JsonSerializable (typeof (AlignmentModes))]
[JsonSerializable (typeof (LineStyle))]
[JsonSerializable (typeof (ShadowStyle))]
[JsonSerializable (typeof (string))]
[JsonSerializable (typeof (bool))]
[JsonSerializable (typeof (bool?))]
[JsonSerializable (typeof (Dictionary<ColorName, string>))]
[JsonSerializable (typeof (Dictionary<string, ThemeScope>))]
[JsonSerializable (typeof (Dictionary<string, ColorScheme>))]
internal partial class SourceGenerationContext : JsonSerializerContext
{ }

View File

@@ -2343,7 +2343,22 @@ internal class WindowsClipboard : ClipboardBase
{
private const uint CF_UNICODE_TEXT = 13;
public override bool IsSupported { get; } = IsClipboardFormatAvailable (CF_UNICODE_TEXT);
public override bool IsSupported { get; } = CheckClipboardIsAvailable ();
private static bool CheckClipboardIsAvailable ()
{
// Attempt to open the clipboard
if (OpenClipboard (nint.Zero))
{
// Clipboard is available
// Close the clipboard after use
CloseClipboard ();
return true;
}
// Clipboard is not available
return false;
}
protected override string GetClipboardDataImpl ()
{

View File

@@ -1,10 +1,11 @@
using System.Text.Json.Serialization;
namespace Terminal.Gui;
/// <summary>
/// Determines the position of items when arranged in a container.
/// </summary>
[JsonConverter (typeof (JsonStringEnumConverter<Alignment>))]
public enum Alignment
{
/// <summary>

View File

@@ -1,10 +1,11 @@
using System.Text.Json.Serialization;
namespace Terminal.Gui;
/// <summary>
/// Determines alignment modes for <see cref="Alignment"/>.
/// </summary>
[JsonConverter (typeof (JsonStringEnumConverter<AlignmentModes>))]
[Flags]
public enum AlignmentModes
{

View File

@@ -284,7 +284,7 @@ public readonly partial record struct Color
),
// Any string too short to possibly be any supported format.
{ Length: > 0 and < 4 } => throw new ColorParseException (
{ Length: > 0 and < 3 } => throw new ColorParseException (
in text,
"Text was too short to be any possible supported format.",
in text

View File

@@ -1,7 +1,10 @@
#nullable enable
using System.Text.Json.Serialization;
namespace Terminal.Gui;
/// <summary>Defines the style of lines for a <see cref="LineCanvas"/>.</summary>
[JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
public enum LineStyle
{
/// <summary>No border is drawn.</summary>

View File

@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json.Serialization;
namespace Terminal.Gui;
@@ -448,9 +449,9 @@ public class Key : EventArgs, IEquatable<Key>
#region String conversion
/// <summary>Pretty prints the KeyEvent</summary>
/// <summary>Pretty prints the Key.</summary>
/// <returns></returns>
public override string ToString () { return ToString (KeyCode, (Rune)'+'); }
public override string ToString () { return ToString (KeyCode, Separator); }
private static string GetKeyString (KeyCode key)
{
@@ -483,7 +484,7 @@ public class Key : EventArgs, IEquatable<Key>
/// The formatted string. If the key is a printable character, it will be returned as a string. Otherwise, the key
/// name will be returned.
/// </returns>
public static string ToString (KeyCode key) { return ToString (key, (Rune)'+'); }
public static string ToString (KeyCode key) { return ToString (key, Separator); }
/// <summary>Formats a <see cref="KeyCode"/> as a string.</summary>
/// <param name="key">The key to format.</param>
@@ -584,7 +585,7 @@ public class Key : EventArgs, IEquatable<Key>
key = null;
// Split the string into parts
string [] parts = text.Split ('+', '-');
string [] parts = text.Split ('+', '-', (char)Separator.Value);
if (parts.Length is 0 or > 4 || parts.Any (string.IsNullOrEmpty))
{
@@ -971,4 +972,20 @@ public class Key : EventArgs, IEquatable<Key>
public static Key F24 => new (KeyCode.F24);
#endregion
private static Rune _separator = new ('+');
/// <summary>Gets or sets the separator character used when parsing and printing Keys. E.g. Ctrl+A. The default is '+'.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Rune Separator
{
get => _separator;
set
{
if (_separator != value)
{
_separator = value == default (Rune) ? new ('+') : value;
}
}
}
}

View File

@@ -23,7 +23,7 @@ public class ShortcutHelper
}
/// <summary>The keystroke combination used in the <see cref="Shortcut"/> as string.</summary>
public virtual string ShortcutTag => Key.ToString (shortcut, MenuBar.ShortcutDelimiter);
public virtual string ShortcutTag => Key.ToString (shortcut, Key.Separator);
/// <summary>Lookup for a <see cref="KeyCode"/> on range of keys.</summary>
/// <param name="key">The source key.</param>
@@ -59,7 +59,7 @@ public class ShortcutHelper
//var hasCtrl = false;
if (delimiter == default (Rune))
{
delimiter = MenuBar.ShortcutDelimiter;
delimiter = Key.Separator;
}
string [] keys = sCut.Split (delimiter.ToString ());

View File

@@ -22,6 +22,7 @@
"Application.NextTabGroupKey": "F6",
"Application.PrevTabGroupKey": "Shift+F6",
"Application.QuitKey": "Esc",
"Key.Separator": "+",
"Theme": "Default",
"Themes": [

View File

@@ -1,8 +1,11 @@
namespace Terminal.Gui;
using System.Text.Json.Serialization;
namespace Terminal.Gui;
/// <summary>
/// Defines the style of shadow to be drawn on the right and bottom sides of the <see cref="View"/>.
/// </summary>
[JsonConverter (typeof (JsonStringEnumConverter<ShadowStyle>))]
public enum ShadowStyle
{
/// <summary>

View File

@@ -5,8 +5,6 @@
// Miguel de Icaza (miguel@gnome.org)
//
using System.Text.Json.Serialization;
namespace Terminal.Gui;
/// <summary>Button is a <see cref="View"/> that provides an item that invokes raises the <see cref="View.Accept"/> event.</summary>
@@ -39,8 +37,6 @@ public class Button : View, IDesignable
/// Gets or sets whether <see cref="Button"/>s are shown with a shadow effect by default.
/// </summary>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<ShadowStyle>))]
public static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None;
/// <summary>Initializes a new instance of <see cref="Button"/>.</summary>

View File

@@ -1,12 +1,9 @@
#nullable enable
namespace Terminal.Gui;
/// <summary>Shows a check box that can be toggled.</summary>
/// <summary>Shows a check box that can be cycled between three states.</summary>
public class CheckBox : View
{
private bool _allowNone;
private CheckState _checked = CheckState.UnChecked;
/// <summary>
/// Initializes a new instance of <see cref="CheckBox"/>.
/// </summary>
@@ -18,8 +15,8 @@ public class CheckBox : View
CanFocus = true;
// Things this view knows how to do
AddCommand (Command.Accept, OnToggle);
AddCommand (Command.HotKey, OnToggle);
AddCommand (Command.Accept, AdvanceCheckState);
AddCommand (Command.HotKey, AdvanceCheckState);
// Default keybindings for this view
KeyBindings.Add (Key.Space, Command.Accept);
@@ -32,7 +29,7 @@ public class CheckBox : View
private void CheckBox_MouseClick (object? sender, MouseEventEventArgs e)
{
e.Handled = OnToggle () == true;
e.Handled = AdvanceCheckState () == true;
}
private void Checkbox_TitleChanged (object? sender, EventArgs<string> e)
@@ -55,8 +52,10 @@ public class CheckBox : View
set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value;
}
private bool _allowNone = false;
/// <summary>
/// If <see langword="true"/> allows <see cref="State"/> to be <see cref="CheckState.None"/>.
/// If <see langword="true"/> allows <see cref="CheckedState"/> to be <see cref="CheckState.None"/>. The default is <see langword="false"/>.
/// </summary>
public bool AllowCheckStateNone
{
@@ -69,13 +68,15 @@ public class CheckBox : View
}
_allowNone = value;
if (State == CheckState.None)
if (CheckedState == CheckState.None)
{
State = CheckState.UnChecked;
CheckedState = CheckState.UnChecked;
}
}
}
private CheckState _checkedState = CheckState.UnChecked;
/// <summary>
/// The state of the <see cref="CheckBox"/>.
/// </summary>
@@ -93,35 +94,42 @@ public class CheckBox : View
/// will display the <c>ConfigurationManager.Glyphs.CheckStateChecked</c> character (☑).
/// </para>
/// </remarks>
public CheckState State
public CheckState CheckedState
{
get => _checked;
get => _checkedState;
set
{
if (_checked == value || (value is CheckState.None && !AllowCheckStateNone))
if (_checkedState == value || (value is CheckState.None && !AllowCheckStateNone))
{
return;
}
_checked = value;
_checkedState = value;
UpdateTextFormatterText ();
OnResizeNeeded ();
}
}
/// <summary>Called when the <see cref="State"/> property changes. Invokes the cancelable <see cref="Toggle"/> event.</summary>
/// <summary>
/// Advances <see cref="CheckedState"/> to the next value. Invokes the cancelable <see cref="CheckedStateChanging"/> event.
/// </summary>
/// <remarks>
/// </remarks>
/// <returns>If <see langword="true"/> the <see cref="Toggle"/> event was canceled.</returns>
/// <returns>If <see langword="true"/> the <see cref="CheckedStateChanging"/> event was canceled.</returns>
/// <remarks>
/// Toggling cycles through the states <see cref="CheckState.None"/>, <see cref="CheckState.Checked"/>, and <see cref="CheckState.UnChecked"/>.
/// <para>
/// Cycles through the states <see cref="CheckState.None"/>, <see cref="CheckState.Checked"/>, and <see cref="CheckState.UnChecked"/>.
/// </para>
/// <para>
/// If the <see cref="CheckedStateChanging"/> event is not canceled, the <see cref="CheckedState"/> will be updated and the <see cref="Command.Accept"/> event will be raised.
/// </para>
/// </remarks>
public bool? OnToggle ()
public bool? AdvanceCheckState ()
{
CheckState oldValue = State;
CancelEventArgs<CheckState> e = new (ref _checked, ref oldValue);
CheckState oldValue = CheckedState;
CancelEventArgs<CheckState> e = new (in _checkedState, ref oldValue);
switch (State)
switch (CheckedState)
{
case CheckState.None:
e.NewValue = CheckState.Checked;
@@ -144,35 +152,35 @@ public class CheckBox : View
break;
}
Toggle?.Invoke (this, e);
CheckedStateChanging?.Invoke (this, e);
if (e.Cancel)
{
return e.Cancel;
}
// By default, Command.Accept calls OnAccept, so we need to call it here to ensure that the event is fired.
// By default, Command.Accept calls OnAccept, so we need to call it here to ensure that the Accept event is fired.
if (OnAccept () == true)
{
return true;
}
State = e.NewValue;
CheckedState = e.NewValue;
return true;
}
/// <summary>Toggle event, raised when the <see cref="CheckBox"/> is toggled.</summary>
/// <summary>Raised when the <see cref="CheckBox"/> state is changing.</summary>
/// <remarks>
/// <para>
/// This event can be cancelled. If cancelled, the <see cref="CheckBox"/> will not change its state.
/// </para>
/// </remarks>
public event EventHandler<CancelEventArgs<CheckState>>? Toggle;
public event EventHandler<CancelEventArgs<CheckState>>? CheckedStateChanging;
/// <inheritdoc/>
protected override void UpdateTextFormatterText ()
{
base.UpdateTextFormatterText();
base.UpdateTextFormatterText ();
switch (TextAlignment)
{
case Alignment.Start:
@@ -190,7 +198,7 @@ public class CheckBox : View
private Rune GetCheckedGlyph ()
{
return State switch
return CheckedState switch
{
CheckState.Checked => Glyphs.CheckStateChecked,
CheckState.UnChecked => Glyphs.CheckStateUnChecked,

View File

@@ -1,6 +1,4 @@
using System.Text.Json.Serialization;
namespace Terminal.Gui;
namespace Terminal.Gui;
/// <summary>
/// The <see cref="Dialog"/> <see cref="View"/> is a <see cref="Window"/> that by default is centered and contains
@@ -19,13 +17,11 @@ public class Dialog : Window
/// <summary>The default <see cref="Alignment"/> for <see cref="Dialog"/>.</summary>
/// <remarks>This property can be set in a Theme.</remarks>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<Alignment>))]
public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; // Default is set in config.json
/// <summary>The default <see cref="Alignment"/> for <see cref="Dialog"/>.</summary>
/// <summary>The default <see cref="AlignmentModes"/> for <see cref="Dialog"/>.</summary>
/// <remarks>This property can be set in a Theme.</remarks>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<AlignmentModes>))]
public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems;
/// <summary>
@@ -47,7 +43,6 @@ public class Dialog : Window
/// Gets or sets whether all <see cref="Window"/>s are shown with a shadow effect by default.
/// </summary>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<ShadowStyle>))]
public new static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None; // Default is set in config.json
/// <summary>
@@ -56,7 +51,6 @@ public class Dialog : Window
/// </summary>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; // Default is set in config.json
private readonly List<Button> _buttons = new ();
@@ -99,6 +93,22 @@ public class Dialog : Window
KeyBindings.Add (Key.Esc, Command.QuitToplevel);
}
// BUGBUG: We override GetNormal/FocusColor because "Dialog" ColorScheme is goofy.
// BUGBUG: By defn, a Dialog is Modal, and thus HasFocus is always true. OnDrawContent
// BUGBUG: Calls these methods.
// TODO: Fix this in https://github.com/gui-cs/Terminal.Gui/issues/2381
/// <inheritdoc />
public override Attribute GetNormalColor ()
{
return ColorScheme.Normal;
}
/// <inheritdoc />
public override Attribute GetFocusColor ()
{
return ColorScheme.Normal;
}
private bool _canceled;
/// <summary>Gets a value indicating whether the <see cref="Dialog"/> was canceled.</summary>
@@ -172,12 +182,5 @@ public class Dialog : Window
_buttons.Add (button);
Add (button);
SetNeedsDisplay ();
if (IsInitialized)
{
LayoutSubviews ();
}
}
}

View File

@@ -1,6 +1,4 @@
using System.Text.Json.Serialization;
namespace Terminal.Gui;
namespace Terminal.Gui;
/// <summary>
/// The FrameView is a container View with a border around it.
@@ -38,6 +36,5 @@ public class FrameView : View
/// <see cref="FrameView"/>s.
/// </remarks>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
}

View File

@@ -120,7 +120,7 @@ internal sealed class Menu : View
}
);
AddKeyBindings (_barItems);
AddKeyBindingsHotKey (_barItems);
}
public Menu ()
@@ -179,7 +179,7 @@ internal sealed class Menu : View
KeyBindings.Add (Key.Enter, Command.Accept);
}
private void AddKeyBindings (MenuBarItem menuBarItem)
private void AddKeyBindingsHotKey (MenuBarItem menuBarItem)
{
if (menuBarItem is null || menuBarItem.Children is null)
{
@@ -190,23 +190,30 @@ internal sealed class Menu : View
{
KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, menuItem);
if ((KeyCode)menuItem.HotKey.Value != KeyCode.Null)
if (menuItem.HotKey != Key.Empty)
{
KeyBindings.Remove ((KeyCode)menuItem.HotKey.Value);
KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, keyBinding);
KeyBindings.Remove ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask);
KeyBindings.Add ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask, keyBinding);
KeyBindings.Remove (menuItem.HotKey);
KeyBindings.Add (menuItem.HotKey, keyBinding);
KeyBindings.Remove (menuItem.HotKey.WithAlt);
KeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding);
}
}
}
if (menuItem.Shortcut != KeyCode.Null)
private void RemoveKeyBindingsHotKey (MenuBarItem menuBarItem)
{
if (menuBarItem is null || menuBarItem.Children is null)
{
return;
}
foreach (MenuItem menuItem in menuBarItem.Children.Where (m => m is { }))
{
if (menuItem.HotKey != Key.Empty)
{
keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
KeyBindings.Remove (menuItem.Shortcut);
KeyBindings.Add (menuItem.Shortcut, keyBinding);
KeyBindings.Remove (menuItem.HotKey);
KeyBindings.Remove (menuItem.HotKey.WithAlt);
}
MenuBarItem subMenu = menuBarItem.SubMenu (menuItem);
AddKeyBindings (subMenu);
}
}
@@ -910,6 +917,8 @@ internal sealed class Menu : View
protected override void Dispose (bool disposing)
{
RemoveKeyBindingsHotKey (_barItems);
if (Application.Current is { })
{
Application.Current.DrawContentComplete -= Current_DrawContentComplete;

View File

@@ -66,6 +66,8 @@ public class MenuBar : View, IDesignable
/// <summary>Initializes a new instance of the <see cref="MenuBar"/>.</summary>
public MenuBar ()
{
MenuItem._menuBar = this;
TabStop = TabBehavior.NoStop;
X = 0;
Y = 0;
@@ -122,7 +124,7 @@ public class MenuBar : View, IDesignable
return true;
}
);
AddCommand (Command.ToggleExpandCollapse, ctx => Select ((int)ctx.KeyBinding?.Context!));
AddCommand (Command.ToggleExpandCollapse, ctx => Select (Menus.IndexOf (ctx.KeyBinding?.Context)));
AddCommand (Command.Select, ctx => Run ((ctx.KeyBinding?.Context as MenuItem)?.Action));
// Default key bindings for this view
@@ -172,19 +174,23 @@ public class MenuBar : View, IDesignable
{
MenuBarItem menuBarItem = Menus [i];
if (menuBarItem?.HotKey != default (Rune))
if (menuBarItem?.HotKey != Key.Empty)
{
KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.Focused, i);
KeyBindings.Add ((KeyCode)menuBarItem.HotKey.Value, keyBinding);
keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, i);
KeyBindings.Add ((KeyCode)menuBarItem.HotKey.Value | KeyCode.AltMask, keyBinding);
KeyBindings.Remove (menuBarItem!.HotKey);
KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.Focused, menuBarItem);
KeyBindings.Add (menuBarItem!.HotKey, keyBinding);
KeyBindings.Remove (menuBarItem.HotKey.WithAlt);
keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, menuBarItem);
KeyBindings.Add (menuBarItem.HotKey.WithAlt, keyBinding);
}
if (menuBarItem?.Shortcut != KeyCode.Null)
if (menuBarItem?.ShortcutKey != Key.Empty)
{
// Technically this will never run because MenuBarItems don't have shortcuts
KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, i);
KeyBindings.Add (menuBarItem.Shortcut, keyBinding);
// unless the IsTopLevel is true
KeyBindings.Remove (menuBarItem.ShortcutKey);
KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuBarItem);
KeyBindings.Add (menuBarItem.ShortcutKey, keyBinding);
}
menuBarItem?.AddShortcutKeyBindings (this);
@@ -1255,21 +1261,6 @@ public class MenuBar : View, IDesignable
}
}
private static Rune _shortcutDelimiter = new ('+');
/// <summary>Sets or gets the shortcut delimiter separator. The default is "+".</summary>
public static Rune ShortcutDelimiter
{
get => _shortcutDelimiter;
set
{
if (_shortcutDelimiter != value)
{
_shortcutDelimiter = value == default (Rune) ? new ('+') : value;
}
}
}
/// <summary>The specifier character for the hot keys.</summary>
public new static Rune HotKeySpecifier => (Rune)'_';
@@ -1321,6 +1312,10 @@ public class MenuBar : View, IDesignable
{
OpenMenu ();
}
else if (Menus [index].IsTopLevel)
{
Run (Menus [index].Action);
}
else
{
Activate (index);
@@ -1766,4 +1761,12 @@ public class MenuBar : View, IDesignable
];
return true;
}
/// <inheritdoc />
protected override void Dispose (bool disposing)
{
MenuItem._menuBar = null;
base.Dispose (disposing);
}
}

View File

@@ -2,7 +2,7 @@ namespace Terminal.Gui;
/// <summary>
/// <see cref="MenuBarItem"/> is a menu item on <see cref="MenuBar"/>. MenuBarItems do not support
/// <see cref="MenuItem.Shortcut"/>.
/// <see cref="MenuItem.ShortcutKey"/>.
/// </summary>
public class MenuBarItem : MenuItem
{
@@ -100,11 +100,9 @@ public class MenuBarItem : MenuItem
{
// For MenuBar only add shortcuts for submenus
if (menuItem.Shortcut != KeyCode.Null)
if (menuItem.ShortcutKey != Key.Empty)
{
KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
menuBar.KeyBindings.Remove (menuItem.Shortcut);
menuBar.KeyBindings.Add (menuItem.Shortcut, keyBinding);
menuItem.UpdateShortcutKeyBinding (Key.Empty);
}
SubMenu (menuItem)?.AddShortcutKeyBindings (menuBar);
@@ -176,4 +174,75 @@ public class MenuBarItem : MenuItem
title ??= string.Empty;
Title = title;
}
/// <summary>
/// Add a <see cref="MenuBarItem"/> dynamically into the <see cref="MenuBar"/><c>.Menus</c>.
/// </summary>
/// <param name="menuItem"></param>
public void AddMenuBarItem (MenuItem menuItem = null)
{
if (menuItem is null)
{
MenuBarItem [] menus = _menuBar.Menus;
Array.Resize (ref menus, menus.Length + 1);
menus [^1] = this;
_menuBar.Menus = menus;
}
else
{
MenuItem [] childrens = Children ?? [];
Array.Resize (ref childrens, childrens.Length + 1);
childrens [^1] = menuItem;
Children = childrens;
}
}
/// <inheritdoc />
public override void RemoveMenuItem ()
{
if (Children is { })
{
foreach (MenuItem menuItem in Children)
{
if (menuItem.ShortcutKey != Key.Empty)
{
// Remove an existent ShortcutKey
_menuBar?.KeyBindings.Remove (menuItem.ShortcutKey);
}
}
}
if (ShortcutKey != Key.Empty)
{
// Remove an existent ShortcutKey
_menuBar?.KeyBindings.Remove (ShortcutKey);
}
var index = _menuBar!.Menus.IndexOf (this);
if (index > -1)
{
if (_menuBar!.Menus [index].HotKey != Key.Empty)
{
// Remove an existent HotKey
_menuBar?.KeyBindings.Remove (HotKey.WithAlt);
}
_menuBar!.Menus [index] = null;
}
var i = 0;
foreach (MenuBarItem m in _menuBar.Menus)
{
if (m != null)
{
_menuBar.Menus [i] = m;
i++;
}
}
MenuBarItem [] menus = _menuBar.Menus;
Array.Resize (ref menus, menus.Length - 1);
_menuBar.Menus = menus;
}
}

View File

@@ -6,31 +6,25 @@ namespace Terminal.Gui;
/// </summary>
public class MenuItem
{
private readonly ShortcutHelper _shortcutHelper;
private bool _allowNullChecked;
private MenuItemCheckStyle _checkType;
internal static MenuBar _menuBar;
private string _title;
// TODO: Update to use Key instead of KeyCode
/// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
public MenuItem (Key shortcutKey = null) : this ("", "", null, null, null, shortcutKey) { }
// TODO: Update to use Key instead of KeyCode
/// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
/// <param name="title">Title for the menu item.</param>
/// <param name="help">Help text to display.</param>
/// <param name="action">Action to invoke when the menu item is activated.</param>
/// <param name="canExecute">Function to determine if the action can currently be executed.</param>
/// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
/// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
/// <param name="shortcutKey">The <see cref="ShortcutKey"/> keystroke combination.</param>
public MenuItem (
string title,
string help,
Action action,
Func<bool> canExecute = null,
MenuItem parent = null,
KeyCode shortcut = KeyCode.Null
Key shortcutKey = null
)
{
Title = title ?? "";
@@ -38,14 +32,20 @@ public class MenuItem
Action = action;
CanExecute = canExecute;
Parent = parent;
_shortcutHelper = new ();
if (shortcut != KeyCode.Null)
if (Parent is { } && Parent.ShortcutKey != Key.Empty)
{
Shortcut = shortcut;
Parent.ShortcutKey = Key.Empty;
}
// Setter will ensure Key.Empty if it's null
ShortcutKey = shortcutKey;
}
private bool _allowNullChecked;
private MenuItemCheckStyle _checkType;
private string _title;
/// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
/// <value>Method to invoke.</value>
public Action Action { get; set; }
@@ -104,6 +104,12 @@ public class MenuItem
/// <value>The help text.</value>
public string Help { get; set; }
/// <summary>
/// Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
/// <see cref="CanExecute"/>.
/// </summary>
public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
/// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
/// <value>The parent.</value>
public MenuItem Parent { get; set; }
@@ -125,46 +131,6 @@ public class MenuItem
}
}
/// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
internal bool IsFromSubMenu => Parent != null;
internal int TitleLength => GetMenuBarItemLength (Title);
//
// ┌─────────────────────────────┐
// │ Quit Quit UI Catalog Ctrl+Q │
// └─────────────────────────────┘
// ┌─────────────────┐
// │ ◌ TopLevel Alt+T │
// └─────────────────┘
// TODO: Replace the `2` literals with named constants
internal int Width => 1
+ // space before Title
TitleLength
+ 2
+ // space after Title - BUGBUG: This should be 1
(Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
? 2
: 0)
+ // check glyph + space
(Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
+ // Two spaces before Help
(ShortcutTag.GetColumns () > 0
? 2 + ShortcutTag.GetColumns ()
: 0); // Pad two spaces before shortcut tag (which are also aligned right)
/// <summary>Merely a debugging aid to see the interaction with main.</summary>
internal bool GetMenuBarItem () { return IsFromSubMenu; }
/// <summary>Merely a debugging aid to see the interaction with main.</summary>
internal MenuItem GetMenuItem () { return this; }
/// <summary>
/// Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
/// <see cref="CanExecute"/>.
/// </summary>
public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
/// <summary>
/// Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
/// <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
@@ -193,6 +159,40 @@ public class MenuItem
}
}
/// <summary>Merely a debugging aid to see the interaction with main.</summary>
internal bool GetMenuBarItem () { return IsFromSubMenu; }
/// <summary>Merely a debugging aid to see the interaction with main.</summary>
internal MenuItem GetMenuItem () { return this; }
/// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
internal bool IsFromSubMenu => Parent != null;
internal int TitleLength => GetMenuBarItemLength (Title);
//
// ┌─────────────────────────────┐
// │ Quit Quit UI Catalog Ctrl+Q │
// └─────────────────────────────┘
// ┌─────────────────┐
// │ ◌ TopLevel Alt+T │
// └─────────────────┘
// TODO: Replace the `2` literals with named constants
internal int Width => 1
+ // space before Title
TitleLength
+ 2
+ // space after Title - BUGBUG: This should be 1
(Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
? 2
: 0)
+ // check glyph + space
(Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
+ // Two spaces before Help
(ShortcutTag.GetColumns () > 0
? 2 + ShortcutTag.GetColumns ()
: 0); // Pad two spaces before shortcut tag (which are also aligned right)
private static int GetMenuBarItemLength (string title)
{
return title.EnumerateRunes ()
@@ -202,21 +202,32 @@ public class MenuItem
#region Keyboard Handling
// TODO: Update to use Key instead of Rune
private Key _hotKey = Key.Empty;
/// <summary>
/// The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
/// <see cref="Title"/> of a MenuItem with an underscore ('_').
/// <para>
/// Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
/// not active). Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
/// not active. Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
/// </para>
/// <para>
/// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
/// File menu. Pressing the N key will then activate the New MenuItem.
/// </para>
/// <para>See also <see cref="Shortcut"/> which enable global key-bindings to menu items.</para>
/// <para>See also <see cref="ShortcutKey"/> which enable global key-bindings to menu items.</para>
/// </summary>
public Rune HotKey { get; set; }
public Key HotKey
{
get => _hotKey;
private set
{
var oldKey = _hotKey ?? Key.Empty;
_hotKey = value ?? Key.Empty;
UpdateHotKeyBinding (oldKey);
}
}
private void GetHotKey ()
{
var nextIsHot = false;
@@ -227,47 +238,130 @@ public class MenuItem
{
nextIsHot = true;
}
else
else if (nextIsHot)
{
if (nextIsHot)
{
HotKey = (Rune)char.ToUpper (x);
HotKey = char.ToLower (x);
break;
}
nextIsHot = false;
HotKey = default (Rune);
return;
}
}
HotKey = Key.Empty;
}
// TODO: Update to use Key instead of KeyCode
private Key _shortcutKey = Key.Empty;
/// <summary>
/// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
/// <see cref="View"/> that is the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this
/// <see cref="MenuItem"/>.
/// <para>
/// The <see cref="KeyCode"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and
/// The <see cref="Key"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and
/// <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
/// </para>
/// </summary>
public KeyCode Shortcut
public Key ShortcutKey
{
get => _shortcutHelper.Shortcut;
get => _shortcutKey;
set
{
if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null))
var oldKey = _shortcutKey ?? Key.Empty;
_shortcutKey = value ?? Key.Empty;
UpdateShortcutKeyBinding (oldKey);
}
}
/// <summary>Gets the text describing the keystroke combination defined by <see cref="ShortcutKey"/>.</summary>
public string ShortcutTag => ShortcutKey != Key.Empty ? ShortcutKey.ToString () : string.Empty;
private void UpdateHotKeyBinding (Key oldKey)
{
if (_menuBar is null || _menuBar?.IsInitialized == false)
{
return;
}
if (oldKey != Key.Empty)
{
var index = _menuBar.Menus?.IndexOf (this);
if (index > -1)
{
_shortcutHelper.Shortcut = value;
_menuBar.KeyBindings.Remove (oldKey.WithAlt);
}
}
if (HotKey != Key.Empty)
{
var index = _menuBar.Menus?.IndexOf (this);
if (index > -1)
{
_menuBar.KeyBindings.Remove (HotKey.WithAlt);
KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, this);
_menuBar.KeyBindings.Add (HotKey.WithAlt, keyBinding);
}
}
}
/// <summary>Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.</summary>
public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null
? string.Empty
: Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
internal void UpdateShortcutKeyBinding (Key oldKey)
{
if (_menuBar is null)
{
return;
}
if (oldKey != Key.Empty)
{
_menuBar.KeyBindings.Remove (oldKey);
}
if (ShortcutKey != Key.Empty)
{
KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, this);
// Remove an existent ShortcutKey
_menuBar?.KeyBindings.Remove (ShortcutKey);
_menuBar?.KeyBindings.Add (ShortcutKey, keyBinding);
}
}
#endregion Keyboard Handling
/// <summary>
/// Removes a <see cref="MenuItem"/> dynamically from the <see cref="Parent"/>.
/// </summary>
public virtual void RemoveMenuItem ()
{
if (Parent is { })
{
MenuItem [] childrens = ((MenuBarItem)Parent).Children;
var i = 0;
foreach (MenuItem c in childrens)
{
if (c != this)
{
childrens [i] = c;
i++;
}
}
Array.Resize (ref childrens, childrens.Length - 1);
if (childrens.Length == 0)
{
((MenuBarItem)Parent).Children = null;
}
else
{
((MenuBarItem)Parent).Children = childrens;
}
}
if (ShortcutKey != Key.Empty)
{
// Remove an existent ShortcutKey
_menuBar?.KeyBindings.Remove (ShortcutKey);
}
}
}

View File

@@ -1,7 +1,4 @@
using System.Diagnostics;
using System.Text.Json.Serialization;
namespace Terminal.Gui;
namespace Terminal.Gui;
/// <summary>
/// MessageBox displays a modal message to the user, with a title, a message and a series of options that the user
@@ -32,13 +29,11 @@ public static class MessageBox
/// <see cref="ConfigurationManager"/>.
/// </summary>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; // Default is set in config.json
/// <summary>The default <see cref="Alignment"/> for <see cref="Dialog"/>.</summary>
/// <remarks>This property can be set in a Theme.</remarks>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<Alignment>))]
public static Alignment DefaultButtonAlignment { get; set; } = Alignment.Center; // Default is set in config.json
/// <summary>
@@ -405,11 +400,6 @@ public static class MessageBox
d.TextFormatter.WordWrap = wrapMessage;
d.TextFormatter.MultiLine = !wrapMessage;
d.ColorScheme = new ColorScheme (d.ColorScheme)
{
Focus = d.ColorScheme.Normal
};
// Setup actions
Clicked = -1;
@@ -423,11 +413,6 @@ public static class MessageBox
Clicked = buttonId;
Application.RequestStop ();
};
if (b.IsDefault)
{
b.SetFocus ();
}
}
// Run the modal; do not shut down the mainloop driver when done

View File

@@ -0,0 +1,251 @@
#nullable enable
using System.ComponentModel;
namespace Terminal.Gui;
/// <summary>
/// Enables the user to increase or decrease a value with the mouse or keyboard.
/// </summary>
/// <remarks>
/// Supports the following types: <see cref="int"/>, <see cref="long"/>, <see cref="double"/>, <see cref="double"/>,
/// <see cref="decimal"/>. Attempting to use any other type will result in an <see cref="InvalidOperationException"/>.
/// </remarks>
public class NumericUpDown<T> : View where T : notnull
{
private readonly Button _down;
// TODO: Use a TextField instead of a Label
private readonly View _number;
private readonly Button _up;
/// <summary>
/// Initializes a new instance of the <see cref="NumericUpDown{T}"/> class.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public NumericUpDown ()
{
Type type = typeof (T);
if (!(type == typeof (object)
|| type == typeof (int)
|| type == typeof (long)
|| type == typeof (double)
|| type == typeof (float)
|| type == typeof (double)
|| type == typeof (decimal)))
{
throw new InvalidOperationException ("T must be a numeric type that supports addition and subtraction.");
}
// `object` is supported only for AllViewsTester
if (type != typeof (object))
{
Increment = (dynamic)1;
Value = (dynamic)0;
}
Width = Dim.Auto (DimAutoStyle.Content);
Height = Dim.Auto (DimAutoStyle.Content);
_down = new ()
{
Height = 1,
Width = 1,
NoPadding = true,
NoDecorations = true,
Title = $"{Glyphs.DownArrow}",
WantContinuousButtonPressed = true,
CanFocus = false,
ShadowStyle = ShadowStyle.None
};
_number = new ()
{
Text = Value?.ToString () ?? "Err",
X = Pos.Right (_down),
Y = Pos.Top (_down),
Width = Dim.Auto (minimumContentDim: Dim.Func (() => string.Format (Format, Value).Length)),
Height = 1,
TextAlignment = Alignment.Center,
CanFocus = true
};
_up = new ()
{
X = Pos.Right (_number),
Y = Pos.Top (_number),
Height = 1,
Width = 1,
NoPadding = true,
NoDecorations = true,
Title = $"{Glyphs.UpArrow}",
WantContinuousButtonPressed = true,
CanFocus = false,
ShadowStyle = ShadowStyle.None
};
CanFocus = true;
_down.Accept += OnDownButtonOnAccept;
_up.Accept += OnUpButtonOnAccept;
Add (_down, _number, _up);
AddCommand (
Command.ScrollUp,
() =>
{
if (type == typeof (object))
{
return false;
}
if (Value is { })
{
Value = (dynamic)Value + (dynamic)Increment;
}
return true;
});
AddCommand (
Command.ScrollDown,
() =>
{
if (type == typeof (object))
{
return false;
}
if (Value is { })
{
Value = (dynamic)Value - (dynamic)Increment;
}
return true;
});
KeyBindings.Add (Key.CursorUp, Command.ScrollUp);
KeyBindings.Add (Key.CursorDown, Command.ScrollDown);
SetText ();
return;
void OnDownButtonOnAccept (object? s, HandledEventArgs e) { InvokeCommand (Command.ScrollDown); }
void OnUpButtonOnAccept (object? s, HandledEventArgs e) { InvokeCommand (Command.ScrollUp); }
}
private T _value = default!;
/// <summary>
/// Gets or sets the value that will be incremented or decremented.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="ValueChanging"/> and <see cref="ValueChanged"/> events are raised when the value changes.
/// The <see cref="ValueChanging"/> event can be canceled the change setting
/// <see cref="CancelEventArgs{T}"/><c>.Cancel</c> to <see langword="true"/>.
/// </para>
/// </remarks>
public T Value
{
get => _value;
set
{
if (_value.Equals (value))
{
return;
}
T oldValue = value;
CancelEventArgs<T> args = new (in _value, ref value);
ValueChanging?.Invoke (this, args);
if (args.Cancel)
{
return;
}
_value = value;
SetText ();
ValueChanged?.Invoke (this, new (in value));
}
}
/// <summary>
/// Raised when the value is about to change. Set <see cref="CancelEventArgs{T}"/><c>.Cancel</c> to true to prevent the
/// change.
/// </summary>
public event EventHandler<CancelEventArgs<T>>? ValueChanging;
/// <summary>
/// Raised when the value has changed.
/// </summary>
public event EventHandler<EventArgs<T>>? ValueChanged;
private string _format = "{0}";
/// <summary>
/// Gets or sets the format string used to display the value. The default is "{0}".
/// </summary>
public string Format
{
get => _format;
set
{
if (_format == value)
{
return;
}
_format = value;
FormatChanged?.Invoke (this, new (value));
SetText ();
}
}
/// <summary>
/// Raised when <see cref="Format"/> has changed.
/// </summary>
public event EventHandler<EventArgs<string>>? FormatChanged;
private void SetText ()
{
_number.Text = string.Format (Format, _value);
Text = _number.Text;
}
private T _increment;
/// <summary>
/// </summary>
public T Increment
{
get => _increment;
set
{
if ((dynamic)_increment == (dynamic)value)
{
return;
}
_increment = value;
IncrementChanged?.Invoke (this, new (value));
}
}
/// <summary>
/// Raised when <see cref="Increment"/> has changed.
/// </summary>
public event EventHandler<EventArgs<T>>? IncrementChanged;
}
/// <summary>
/// Enables the user to increase or decrease an <see langword="int"/> by clicking on the up or down buttons.
/// </summary>
public class NumericUpDown : NumericUpDown<int>
{ }

View File

@@ -1,6 +1,4 @@
using System.Text.Json.Serialization;
namespace Terminal.Gui;
namespace Terminal.Gui;
/// <summary>
/// A <see cref="Toplevel"/> <see cref="View"/> with <see cref="View.BorderStyle"/> set to
@@ -75,6 +73,5 @@ public class Window : Toplevel
/// s.
/// </remarks>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
[JsonConverter (typeof (JsonStringEnumConverter<LineStyle>))]
public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
}