mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-01-02 01:03:29 +01:00
Merge branch 'v2_develop' into v2_2491-Arrangement-Overlapped
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{ }
|
||||
|
||||
@@ -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 ()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ());
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"Application.NextTabGroupKey": "F6",
|
||||
"Application.PrevTabGroupKey": "Shift+F6",
|
||||
"Application.QuitKey": "Esc",
|
||||
"Key.Separator": "+",
|
||||
|
||||
"Theme": "Default",
|
||||
"Themes": [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
251
Terminal.Gui/Views/NumericUpDown.cs
Normal file
251
Terminal.Gui/Views/NumericUpDown.cs
Normal 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>
|
||||
{ }
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user