mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Cosmetic changes, UX/UI improvements in TUI mode, added colored text support in dialogs
This commit is contained in:
parent
2aa85cbb17
commit
c0d983272b
9 changed files with 321 additions and 105 deletions
|
|
@ -1,12 +1,81 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Console backend wrapping the ConsoleInteractive library (existing behavior).
|
||||
/// </summary>
|
||||
public class ClassicConsoleBackend : IConsoleBackend
|
||||
public partial class ClassicConsoleBackend : IConsoleBackend
|
||||
{
|
||||
private static readonly (byte R, byte G, byte B, char Code)[] McStandardColors =
|
||||
[
|
||||
(0, 0, 0, '0'), // black
|
||||
(0, 0, 170, '1'), // dark_blue
|
||||
(0, 170, 0, '2'), // dark_green
|
||||
(0, 170, 170, '3'), // dark_aqua
|
||||
(170, 0, 0, '4'), // dark_red
|
||||
(170, 0, 170, '5'), // dark_purple
|
||||
(255, 170, 0, '6'), // gold
|
||||
(170, 170, 170, '7'), // gray
|
||||
(85, 85, 85, '8'), // dark_gray
|
||||
(85, 85, 255, '9'), // blue
|
||||
(85, 255, 85, 'a'), // green
|
||||
(85, 255, 255, 'b'), // aqua
|
||||
(255, 85, 85, 'c'), // red
|
||||
(255, 85, 255, 'd'), // light_purple
|
||||
(255, 255, 85, 'e'), // yellow
|
||||
(255, 255, 255, 'f'), // white
|
||||
];
|
||||
|
||||
[GeneratedRegex("§#([0-9a-fA-F]{6})")]
|
||||
private static partial Regex HexColorRegex();
|
||||
|
||||
private static char NearestMcColor(byte r, byte g, byte b)
|
||||
{
|
||||
int bestIdx = 0;
|
||||
long bestDist = long.MaxValue;
|
||||
|
||||
for (int i = 0; i < McStandardColors.Length; i++)
|
||||
{
|
||||
var (sr, sg, sb, _) = McStandardColors[i];
|
||||
long dr = r - sr;
|
||||
long dg = g - sg;
|
||||
long db = b - sb;
|
||||
long dist = dr * dr + dg * dg + db * db;
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
return McStandardColors[bestIdx].Code;
|
||||
}
|
||||
|
||||
private static string ResolveHexColors(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || !text.Contains("§#", StringComparison.Ordinal))
|
||||
return text;
|
||||
|
||||
return HexColorRegex().Replace(text, match =>
|
||||
{
|
||||
ReadOnlySpan<char> hex = match.Groups[1].ValueSpan;
|
||||
byte r = (byte)((HexVal(hex[0]) << 4) | HexVal(hex[1]));
|
||||
byte g = (byte)((HexVal(hex[2]) << 4) | HexVal(hex[3]));
|
||||
byte b = (byte)((HexVal(hex[4]) << 4) | HexVal(hex[5]));
|
||||
return $"§{NearestMcColor(r, g, b)}";
|
||||
});
|
||||
}
|
||||
|
||||
private static int HexVal(char c) => c switch
|
||||
{
|
||||
>= '0' and <= '9' => c - '0',
|
||||
>= 'a' and <= 'f' => c - 'a' + 10,
|
||||
>= 'A' and <= 'F' => c - 'A' + 10,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
public event EventHandler<string>? MessageReceived;
|
||||
public event EventHandler<ConsoleInputBuffer>? OnInputChange;
|
||||
|
||||
|
|
@ -28,7 +97,7 @@ namespace MinecraftClient
|
|||
|
||||
public void WriteLineFormatted(string text)
|
||||
{
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(text);
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(ResolveHexColors(text));
|
||||
}
|
||||
|
||||
public void BeginReadThread()
|
||||
|
|
|
|||
|
|
@ -53,9 +53,11 @@ public class Dialog : Command
|
|||
{
|
||||
var handler = CmdResult.currentHandler!;
|
||||
var current = handler.Dialogs.Current;
|
||||
return current is null
|
||||
? r.SetAndReturn(CmdResult.Status.Fail, Translations.dialog_none)
|
||||
: r.SetAndReturn(CmdResult.Status.Done, DialogFormatter.Render(current));
|
||||
if (current is null)
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, Translations.dialog_none);
|
||||
|
||||
ConsoleIO.WriteLineFormatted(DialogFormatter.Render(current), acceptnewlines: true);
|
||||
return r.SetAndReturn(CmdResult.Status.Done);
|
||||
}
|
||||
|
||||
private static int Open(CmdResult r)
|
||||
|
|
|
|||
|
|
@ -11,17 +11,21 @@ public static class DialogFormatter
|
|||
if (!string.IsNullOrWhiteSpace(definition.ExternalTitle))
|
||||
return definition.ExternalTitle!;
|
||||
|
||||
return string.IsNullOrWhiteSpace(definition.Title) ? definition.Type : definition.Title;
|
||||
return string.IsNullOrWhiteSpace(definition.Title) ? DisplayType(definition.Type) : definition.Title;
|
||||
}
|
||||
|
||||
private const int BoxWidth = 50;
|
||||
|
||||
public static string Render(DialogInstance instance)
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.AppendLine(string.Format(Translations.dialog_render_header, instance.Revision, instance.Phase, instance.Definition.DisplayTitle()));
|
||||
builder.AppendLine(string.Format(Translations.dialog_render_type, DisplayType(instance.Definition.Type)));
|
||||
string border = new('-', BoxWidth);
|
||||
builder.AppendLine(border);
|
||||
builder.AppendLine(" " + string.Format(Translations.dialog_render_header, instance.Revision, instance.Phase, instance.Definition.DisplayTitle()));
|
||||
builder.AppendLine(border);
|
||||
|
||||
foreach (var body in instance.Definition.Body.Where(static body => !string.IsNullOrWhiteSpace(body.Text)))
|
||||
builder.AppendLine(string.Format(Translations.dialog_render_body, body.Text));
|
||||
builder.AppendLine(body.Text);
|
||||
|
||||
if (instance.Definition.Inputs.Count > 0)
|
||||
{
|
||||
|
|
@ -30,7 +34,7 @@ public static class DialogFormatter
|
|||
{
|
||||
instance.Values.TryGetValue(input.Key, out var value);
|
||||
value ??= input.InitialValue;
|
||||
builder.AppendLine(string.Format(Translations.dialog_render_input, input.Key, input.Kind, input.Label, value, DescribeInput(input)));
|
||||
builder.AppendLine(string.Format(Translations.dialog_render_input, input.Key, DescribeKind(input.Kind), input.Label, value, DescribeInput(input)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,9 +45,7 @@ public static class DialogFormatter
|
|||
builder.AppendLine(string.Format(Translations.dialog_render_action, action.Index, action.Label, DescribeAction(action.Action)));
|
||||
}
|
||||
|
||||
if (instance.Definition.CancelAction is not null || instance.Definition.CanCloseWithEscape)
|
||||
builder.AppendLine(Translations.dialog_render_cancel_hint);
|
||||
|
||||
builder.Append(border);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +62,18 @@ public static class DialogFormatter
|
|||
};
|
||||
}
|
||||
|
||||
private static string DescribeKind(DialogInputKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
DialogInputKind.Text => Translations.dialog_input_kind_text,
|
||||
DialogInputKind.Boolean => Translations.dialog_input_kind_boolean,
|
||||
DialogInputKind.SingleOption => Translations.dialog_input_kind_options,
|
||||
DialogInputKind.NumberRange => Translations.dialog_input_kind_number,
|
||||
_ => Translations.dialog_input_kind_unknown
|
||||
};
|
||||
}
|
||||
|
||||
private static string DescribeInput(DialogInput input)
|
||||
{
|
||||
return input.Kind switch
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public sealed class DialogManager
|
|||
_current = instance;
|
||||
}
|
||||
|
||||
_client.Log.Info(string.Format(Translations.dialog_received, instance.Definition.DisplayTitle()));
|
||||
_client.Log.Info("§e" + string.Format(Translations.dialog_received, instance.Definition.DisplayTitle()));
|
||||
DialogShown?.Invoke(instance);
|
||||
return instance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,9 +79,6 @@ public sealed class DialogNbtParser
|
|||
var body = ParseBody(GetValue(nbt, "body"));
|
||||
var inputs = ParseInputs(GetValue(nbt, "inputs"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
title = type;
|
||||
|
||||
return new DialogCommon(title, externalTitle, canCloseWithEscape, pause, afterAction, body, inputs);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8101,5 +8101,25 @@ namespace MinecraftClient {
|
|||
get { return ResourceManager.GetString("dialog.type.unknown", resourceCulture); }
|
||||
}
|
||||
|
||||
internal static string dialog_input_kind_text {
|
||||
get { return ResourceManager.GetString("dialog.input_kind.text", resourceCulture); }
|
||||
}
|
||||
|
||||
internal static string dialog_input_kind_boolean {
|
||||
get { return ResourceManager.GetString("dialog.input_kind.boolean", resourceCulture); }
|
||||
}
|
||||
|
||||
internal static string dialog_input_kind_options {
|
||||
get { return ResourceManager.GetString("dialog.input_kind.options", resourceCulture); }
|
||||
}
|
||||
|
||||
internal static string dialog_input_kind_number {
|
||||
get { return ResourceManager.GetString("dialog.input_kind.number", resourceCulture); }
|
||||
}
|
||||
|
||||
internal static string dialog_input_kind_unknown {
|
||||
get { return ResourceManager.GetString("dialog.input_kind.unknown", resourceCulture); }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2834,7 +2834,7 @@ see item details.</value>
|
|||
<value>View and interact with the current server custom dialog.</value>
|
||||
</data>
|
||||
<data name="dialog.received" xml:space="preserve">
|
||||
<value>Server showed custom dialog: {0}. Use dialog show.</value>
|
||||
<value>Server showed custom dialog: {0}. Use /dialog show.</value>
|
||||
</data>
|
||||
<data name="dialog.cleared" xml:space="preserve">
|
||||
<value>Server cleared the custom dialog.</value>
|
||||
|
|
@ -2993,13 +2993,13 @@ see item details.</value>
|
|||
<value> [{0}] {1} ({2})</value>
|
||||
</data>
|
||||
<data name="dialog.render.cancel_hint" xml:space="preserve">
|
||||
<value>Use dialog cancel to close or run the cancel action.</value>
|
||||
<value>Use /dialog cancel to close or run the cancel action.</value>
|
||||
</data>
|
||||
<data name="dialog.input_desc_text" xml:space="preserve">
|
||||
<value>max {0} chars</value>
|
||||
</data>
|
||||
<data name="dialog.input_desc_boolean" xml:space="preserve">
|
||||
<value>true={0}, false={1}</value>
|
||||
<value>Yes={0}, No={1}</value>
|
||||
</data>
|
||||
<data name="dialog.input_desc_options" xml:space="preserve">
|
||||
<value>options: {0}</value>
|
||||
|
|
@ -3041,7 +3041,7 @@ see item details.</value>
|
|||
<value>Confirmation</value>
|
||||
</data>
|
||||
<data name="dialog.type.multi_action" xml:space="preserve">
|
||||
<value>Multi-action</value>
|
||||
<value>Multi Action</value>
|
||||
</data>
|
||||
<data name="dialog.type.dialog_list" xml:space="preserve">
|
||||
<value>Dialog list</value>
|
||||
|
|
@ -3052,4 +3052,19 @@ see item details.</value>
|
|||
<data name="dialog.type.unknown" xml:space="preserve">
|
||||
<value>Unknown</value>
|
||||
</data>
|
||||
<data name="dialog.input_kind.text" xml:space="preserve">
|
||||
<value>Text</value>
|
||||
</data>
|
||||
<data name="dialog.input_kind.boolean" xml:space="preserve">
|
||||
<value>Check box</value>
|
||||
</data>
|
||||
<data name="dialog.input_kind.options" xml:space="preserve">
|
||||
<value>Options</value>
|
||||
</data>
|
||||
<data name="dialog.input_kind.number" xml:space="preserve">
|
||||
<value>Number</value>
|
||||
</data>
|
||||
<data name="dialog.input_kind.unknown" xml:space="preserve">
|
||||
<value>Unknown</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -59,20 +59,27 @@ public static class DialogTuiHost
|
|||
|
||||
internal sealed class DialogView : Border, IOverlayCloseHandler
|
||||
{
|
||||
private static readonly Color AccentColor = Color.FromRgb(80, 180, 255);
|
||||
private static readonly Color BorderColor = Color.FromRgb(70, 70, 70);
|
||||
private static readonly Color SectionBg = Color.FromRgb(20, 20, 20);
|
||||
private static readonly Color InputBg = Color.FromRgb(35, 35, 35);
|
||||
|
||||
private readonly McClient _handler;
|
||||
private readonly DialogInstance _instance;
|
||||
private readonly TextBlock _status;
|
||||
private readonly Dictionary<string, Control> _inputControls = new(StringComparer.Ordinal);
|
||||
private readonly StackPanel _inputsPanel;
|
||||
private readonly WrapPanel _buttonsPanel;
|
||||
|
||||
public DialogView(McClient handler, DialogInstance instance)
|
||||
{
|
||||
_handler = handler;
|
||||
_instance = instance;
|
||||
|
||||
BorderBrush = Brushes.White;
|
||||
BorderBrush = new SolidColorBrush(BorderColor);
|
||||
BorderThickness = new Thickness(1);
|
||||
Background = Brushes.Black;
|
||||
Padding = new Thickness(1);
|
||||
Background = new SolidColorBrush(Color.FromRgb(12, 12, 12));
|
||||
Padding = new Thickness(2);
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalAlignment = VerticalAlignment.Stretch;
|
||||
Focusable = true;
|
||||
|
|
@ -81,14 +88,18 @@ internal sealed class DialogView : Border, IOverlayCloseHandler
|
|||
{
|
||||
Foreground = Brushes.Gray,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(1, 0)
|
||||
Margin = new Thickness(0, 1, 0, 0)
|
||||
};
|
||||
|
||||
_inputsPanel = new StackPanel { Spacing = 0, Margin = new Thickness(0) };
|
||||
_buttonsPanel = new WrapPanel { Orientation = Orientation.Horizontal };
|
||||
|
||||
Child = BuildContent();
|
||||
|
||||
AttachedToVisualTree += (_, _) =>
|
||||
{
|
||||
AddHandler(KeyDownEvent, OnTunnelKeyDown, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||
FocusFirstInput();
|
||||
Focus();
|
||||
};
|
||||
DetachedFromVisualTree += (_, _) => RemoveHandler(KeyDownEvent, OnTunnelKeyDown);
|
||||
|
|
@ -112,75 +123,97 @@ internal sealed class DialogView : Border, IOverlayCloseHandler
|
|||
|
||||
private Control BuildContent()
|
||||
{
|
||||
var main = new StackPanel
|
||||
var root = new DockPanel { Margin = new Thickness(0) };
|
||||
|
||||
var scroll = new ScrollViewer
|
||||
{
|
||||
Spacing = 1,
|
||||
Margin = new Thickness(1)
|
||||
HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto
|
||||
};
|
||||
|
||||
main.Children.Add(new TextBlock
|
||||
var main = new StackPanel { Spacing = 0, Margin = new Thickness(0) };
|
||||
|
||||
// Title
|
||||
main.Children.Add(McColorParser.CreateColoredTextBlock(_instance.Definition.DisplayTitle()));
|
||||
|
||||
// Separator
|
||||
main.Children.Add(new Border
|
||||
{
|
||||
Text = _instance.Definition.DisplayTitle(),
|
||||
Foreground = Brushes.Yellow,
|
||||
FontWeight = FontWeight.Bold,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
Height = 1,
|
||||
Background = new SolidColorBrush(BorderColor),
|
||||
Margin = new Thickness(0, 1, 0, 1)
|
||||
});
|
||||
|
||||
// Body
|
||||
foreach (var body in _instance.Definition.Body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body.Text))
|
||||
continue;
|
||||
main.Children.Add(McColorParser.CreateColoredTextBlock(body.Text));
|
||||
}
|
||||
|
||||
// Build action buttons
|
||||
EnsureActionButtons();
|
||||
|
||||
// Inputs
|
||||
foreach (var input in _instance.Definition.Inputs)
|
||||
main.Children.Add(BuildInput(input));
|
||||
|
||||
// Action buttons
|
||||
if (_buttonsPanel.Children.Count > 0)
|
||||
main.Children.Add(_buttonsPanel);
|
||||
|
||||
// Cancel hint
|
||||
if (_instance.Definition.CancelAction is not null || _instance.Definition.CanCloseWithEscape)
|
||||
{
|
||||
main.Children.Add(new TextBlock
|
||||
{
|
||||
Text = body.Text,
|
||||
Foreground = Brushes.White,
|
||||
Text = Translations.dialog_render_cancel_hint,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(120, 120, 120)),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var input in _instance.Definition.Inputs)
|
||||
main.Children.Add(BuildInput(input));
|
||||
main.Children.Add(_status);
|
||||
scroll.Content = main;
|
||||
root.Children.Add(scroll);
|
||||
|
||||
var buttons = new WrapPanel
|
||||
return root;
|
||||
}
|
||||
|
||||
private void EnsureActionButtons()
|
||||
{
|
||||
Orientation = Orientation.Horizontal
|
||||
};
|
||||
if (_buttonsPanel.Children.Count > 0)
|
||||
return;
|
||||
|
||||
foreach (var action in _instance.Definition.Actions)
|
||||
{
|
||||
var button = new Button
|
||||
var btn = new Button
|
||||
{
|
||||
Content = action.Label,
|
||||
Margin = new Thickness(0, 0, 1, 1),
|
||||
MinWidth = Math.Max(8, Math.Min(action.Label.Length + 4, 32))
|
||||
Content = McColorParser.CreateColoredTextBlock(action.Label),
|
||||
Padding = new Thickness(1),
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(60, 60, 60)),
|
||||
Background = new SolidColorBrush(Color.FromRgb(40, 40, 40)),
|
||||
Margin = new Thickness(0, 0, 1, 1)
|
||||
};
|
||||
button.Click += (_, _) => Click(action.Index);
|
||||
buttons.Children.Add(button);
|
||||
btn.Click += (_, _) => Click(action.Index);
|
||||
_buttonsPanel.Children.Add(btn);
|
||||
}
|
||||
|
||||
if (_instance.Definition.CancelAction is not null || _instance.Definition.CanCloseWithEscape)
|
||||
{
|
||||
var cancel = new Button
|
||||
{
|
||||
Content = Translations.tui_dialog_cancel,
|
||||
Margin = new Thickness(0, 0, 1, 1)
|
||||
Content = McColorParser.CreateColoredTextBlock(Translations.tui_dialog_cancel),
|
||||
Padding = new Thickness(1),
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(80, 40, 40)),
|
||||
Background = new SolidColorBrush(Color.FromRgb(50, 25, 25))
|
||||
};
|
||||
cancel.Click += (_, _) => TryCloseByUser();
|
||||
buttons.Children.Add(cancel);
|
||||
_buttonsPanel.Children.Add(cancel);
|
||||
}
|
||||
|
||||
if (buttons.Children.Count > 0)
|
||||
main.Children.Add(buttons);
|
||||
|
||||
main.Children.Add(_status);
|
||||
|
||||
return new ScrollViewer
|
||||
{
|
||||
Content = main,
|
||||
HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildInput(DialogInput input)
|
||||
|
|
@ -188,84 +221,125 @@ internal sealed class DialogView : Border, IOverlayCloseHandler
|
|||
_instance.Values.TryGetValue(input.Key, out var value);
|
||||
value ??= input.InitialValue;
|
||||
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
Margin = new Thickness(0, 1)
|
||||
};
|
||||
var panel = new StackPanel { Spacing = 0, Margin = new Thickness(0) };
|
||||
|
||||
if (input.LabelVisible && !string.IsNullOrWhiteSpace(input.Label))
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = input.Label,
|
||||
Foreground = Brushes.LightGray,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
}
|
||||
panel.Children.Add(McColorParser.CreateColoredTextBlock(input.Label));
|
||||
|
||||
Control control = input.Kind switch
|
||||
Control inner = input.Kind switch
|
||||
{
|
||||
DialogInputKind.Boolean => BuildBooleanInput(value),
|
||||
DialogInputKind.Boolean => BuildBooleanInput(value, input),
|
||||
DialogInputKind.SingleOption => BuildOptionInput(input, value),
|
||||
DialogInputKind.NumberRange => BuildNumberInput(input, value),
|
||||
_ => BuildTextInput(input, value)
|
||||
};
|
||||
|
||||
_inputControls[input.Key] = control;
|
||||
panel.Children.Add(control);
|
||||
_inputControls[input.Key] = inner;
|
||||
|
||||
if (input.Kind == DialogInputKind.Boolean)
|
||||
{
|
||||
panel.Children.Add(inner);
|
||||
}
|
||||
else
|
||||
{
|
||||
panel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(InputBg),
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(55, 55, 55)),
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(1),
|
||||
Child = inner
|
||||
});
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static Control BuildTextInput(DialogInput input, string value)
|
||||
private Control BuildTextInput(DialogInput input, string value)
|
||||
{
|
||||
return new TextBox
|
||||
var tb = new TextBox
|
||||
{
|
||||
Text = value,
|
||||
AcceptsReturn = input.Multiline,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
TextWrapping = input.Multiline ? TextWrapping.Wrap : TextWrapping.NoWrap,
|
||||
MaxLength = input.MaxLength,
|
||||
Foreground = Brushes.White,
|
||||
Background = Brushes.Black,
|
||||
BorderBrush = Brushes.Gray
|
||||
Background = new SolidColorBrush(InputBg),
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(0)
|
||||
};
|
||||
|
||||
return tb;
|
||||
}
|
||||
|
||||
private static Control BuildBooleanInput(string value)
|
||||
private Control BuildBooleanInput(string value, DialogInput input)
|
||||
{
|
||||
return new CheckBox
|
||||
{
|
||||
IsChecked = value.Equals("true", StringComparison.OrdinalIgnoreCase),
|
||||
Foreground = Brushes.White
|
||||
Foreground = Brushes.White,
|
||||
Padding = new Thickness(0)
|
||||
};
|
||||
}
|
||||
|
||||
private static Control BuildOptionInput(DialogInput input, string value)
|
||||
private Control BuildOptionInput(DialogInput input, string value)
|
||||
{
|
||||
var combo = new ComboBox
|
||||
{
|
||||
ItemsSource = input.Options ?? [],
|
||||
Foreground = Brushes.White
|
||||
Foreground = Brushes.White,
|
||||
Background = new SolidColorBrush(InputBg),
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(1, 0)
|
||||
};
|
||||
combo.SelectionBoxItemTemplate = null;
|
||||
|
||||
combo.SelectedItem = input.Options?.FirstOrDefault(option => option.Id.Equals(value, StringComparison.Ordinal))
|
||||
?? input.Options?.FirstOrDefault();
|
||||
|
||||
return combo;
|
||||
}
|
||||
|
||||
private static Control BuildNumberInput(DialogInput input, string value)
|
||||
private Control BuildNumberInput(DialogInput input, string value)
|
||||
{
|
||||
double numValue = double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
|
||||
? parsed
|
||||
: input.InitialNumber ?? input.Start;
|
||||
|
||||
double min = Math.Min(input.Start, input.End);
|
||||
double max = Math.Max(input.Start, input.End);
|
||||
|
||||
var panel = new DockPanel { LastChildFill = true };
|
||||
|
||||
var slider = new Slider
|
||||
{
|
||||
Minimum = Math.Min(input.Start, input.End),
|
||||
Maximum = Math.Max(input.Start, input.End),
|
||||
Value = double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var number)
|
||||
? number
|
||||
: input.InitialNumber ?? input.Start,
|
||||
Minimum = min,
|
||||
Maximum = max,
|
||||
Value = numValue,
|
||||
TickFrequency = input.Step ?? 1,
|
||||
IsSnapToTickEnabled = input.Step is not null
|
||||
IsSnapToTickEnabled = input.Step is not null,
|
||||
Foreground = new SolidColorBrush(AccentColor)
|
||||
};
|
||||
return slider;
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = numValue.ToString(CultureInfo.InvariantCulture),
|
||||
Foreground = Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(2, 0, 0, 0),
|
||||
MinWidth = 16
|
||||
};
|
||||
|
||||
slider.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.Property == Slider.ValueProperty)
|
||||
label.Text = ((float)slider.Value).ToString(CultureInfo.InvariantCulture);
|
||||
};
|
||||
|
||||
DockPanel.SetDock(label, Dock.Right);
|
||||
panel.Children.Add(slider);
|
||||
panel.Children.Add(label);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void Click(int index)
|
||||
|
|
@ -306,6 +380,20 @@ internal sealed class DialogView : Border, IOverlayCloseHandler
|
|||
return true;
|
||||
}
|
||||
|
||||
private void FocusFirstInput()
|
||||
{
|
||||
var first = _inputControls.Values.FirstOrDefault();
|
||||
if (first is TextBox tb)
|
||||
{
|
||||
tb.Focus();
|
||||
tb.SelectAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
first?.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseIfInactive()
|
||||
{
|
||||
var current = _handler.Dialogs.Current;
|
||||
|
|
@ -326,11 +414,22 @@ internal sealed class DialogView : Border, IOverlayCloseHandler
|
|||
|
||||
private void OnTunnelKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key != Key.Escape)
|
||||
return;
|
||||
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
TryCloseByUser();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
var focused = TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement();
|
||||
if (focused is TextBox && _instance.Definition.Actions.Count > 0)
|
||||
{
|
||||
Click(_instance.Definition.Actions[0].Index);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string NumberToString(float value)
|
||||
|
|
|
|||
|
|
@ -151,14 +151,14 @@ header "1. Notice Dialog"
|
|||
mc-rcon 'dialog show MCCBot {type:"minecraft:notice", title:{text:"Notice Title"}}' 2>&1 | ansi_strip | grep -v "^$"
|
||||
assert_dialog_shown "notice dialog" "Notice Title"
|
||||
write_input "dialog show"
|
||||
assert_log "notice type" "Type: Notice" 10
|
||||
assert_log "notice render" "Dialog #1" 10
|
||||
assert_log "notice OK button" "OK (close)" 3
|
||||
|
||||
header "2. Confirmation Dialog"
|
||||
mc-rcon 'dialog show MCCBot {type:"minecraft:confirmation", title:{text:"Confirm?"}, yes:{label:{text:"Yes"}}, no:{label:{text:"No"}}}' 2>&1 | ansi_strip | grep -v "^$"
|
||||
assert_dialog_shown "confirmation" "Confirm?"
|
||||
write_input "dialog show"
|
||||
assert_log "confirmation type" "Type: Confirmation" 10
|
||||
assert_log "confirmation render" "Dialog #1" 10
|
||||
assert_log "yes button" "Yes (close)" 3
|
||||
assert_log "no button" "No (close)" 3
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ header "3. Multi-Action Dialog"
|
|||
mc-rcon 'dialog show MCCBot {type:"minecraft:multi_action", title:{text:"Choose"}, actions:[{label:{text:"Alpha"}}, {label:{text:"Beta"}}, {label:{text:"Gamma"}}]}' 2>&1 | ansi_strip | grep -v "^$"
|
||||
assert_dialog_shown "multi_action" "Choose"
|
||||
write_input "dialog show"
|
||||
assert_log "multi_action type" "Type: Multi-action" 10
|
||||
assert_log "multi_action render" "Dialog #1" 10
|
||||
assert_log "multi_action button 1" "Alpha (close)" 3
|
||||
assert_log "multi_action button 2" "Beta (close)" 3
|
||||
assert_log "multi_action button 3" "Gamma (close)" 3
|
||||
|
|
@ -175,7 +175,7 @@ header "4. Dialog-List Dialog"
|
|||
mc-rcon 'dialog show MCCBot {type:"minecraft:dialog_list", title:{text:"List"}, dialogs:[{type:"minecraft:notice", title:{text:"Sub One"}}, {type:"minecraft:notice", title:{text:"Sub Two"}}]}' 2>&1 | ansi_strip | grep -v "^$"
|
||||
assert_dialog_shown "dialog_list" "List"
|
||||
write_input "dialog show"
|
||||
assert_log "dialog_list type" "Type: Dialog list" 10
|
||||
assert_log "dialog_list render" "Dialog #1" 10
|
||||
assert_log "dialog_list sub 1" "Sub One (show dialog)" 3
|
||||
assert_log "dialog_list sub 2" "Sub Two (show dialog)" 3
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ header "5. Server-Links Dialog"
|
|||
mc-rcon 'dialog show MCCBot {type:"minecraft:server_links", title:{text:"Links"}}' 2>&1 | ansi_strip | grep -v "^$"
|
||||
assert_dialog_shown "server_links" "Links"
|
||||
write_input "dialog show"
|
||||
assert_log "server_links type" "Type: Server links" 10
|
||||
assert_log "server_links render" "Dialog #1" 10
|
||||
|
||||
header "6. Body Content"
|
||||
mc-rcon 'dialog show MCCBot {type:"minecraft:notice", title:{text:"With Body"}, body:[{type:"minecraft:plain_message", contents:{text:"Hello from body"}}]}' 2>&1 | ansi_strip | grep -v "^$"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue