mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
442 lines
13 KiB
C#
442 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using MinecraftClient.Dialogs;
|
|
|
|
namespace MinecraftClient.Tui;
|
|
|
|
internal interface IOverlayCloseHandler
|
|
{
|
|
bool TryCloseByUser();
|
|
}
|
|
|
|
public static class DialogTuiHost
|
|
{
|
|
public static bool TryOpen(McClient handler, DialogInstance instance, bool force)
|
|
{
|
|
if (ConsoleIO.Backend is not TuiConsoleBackend)
|
|
return false;
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
var view = TuiConsoleBackend.Instance?.GetView();
|
|
if (view is null)
|
|
return;
|
|
|
|
if (view.HasOverlay && view.OverlayContent is not DialogView && !force)
|
|
{
|
|
handler.Log.Info(Translations.dialog_tui_pending);
|
|
return;
|
|
}
|
|
|
|
view.ShowOverlay(new DialogView(handler, instance));
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
public static void CloseCurrent()
|
|
{
|
|
if (ConsoleIO.Backend is not TuiConsoleBackend)
|
|
return;
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
var view = TuiConsoleBackend.Instance?.GetView();
|
|
if (view?.OverlayContent is DialogView)
|
|
view.HideOverlay();
|
|
});
|
|
}
|
|
}
|
|
|
|
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 = new SolidColorBrush(BorderColor);
|
|
BorderThickness = new Thickness(1);
|
|
Background = new SolidColorBrush(Color.FromRgb(12, 12, 12));
|
|
Padding = new Thickness(2);
|
|
HorizontalAlignment = HorizontalAlignment.Stretch;
|
|
VerticalAlignment = VerticalAlignment.Stretch;
|
|
Focusable = true;
|
|
|
|
_status = new TextBlock
|
|
{
|
|
Foreground = Brushes.Gray,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
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);
|
|
}
|
|
|
|
public bool TryCloseByUser()
|
|
{
|
|
if (!_instance.Definition.CanCloseWithEscape && _instance.Definition.CancelAction is null)
|
|
{
|
|
SetStatus(Translations.dialog_cannot_cancel);
|
|
return false;
|
|
}
|
|
|
|
var result = _handler.Dialogs.Cancel();
|
|
SetStatus(result.Message);
|
|
if (result.Success)
|
|
CloseIfInactive();
|
|
|
|
return false;
|
|
}
|
|
|
|
private Control BuildContent()
|
|
{
|
|
var root = new DockPanel { Margin = new Thickness(0) };
|
|
|
|
var scroll = new ScrollViewer
|
|
{
|
|
HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Disabled,
|
|
VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto
|
|
};
|
|
|
|
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
|
|
{
|
|
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 = Translations.dialog_render_cancel_hint,
|
|
Foreground = new SolidColorBrush(Color.FromRgb(120, 120, 120)),
|
|
TextWrapping = TextWrapping.Wrap
|
|
});
|
|
}
|
|
|
|
main.Children.Add(_status);
|
|
scroll.Content = main;
|
|
root.Children.Add(scroll);
|
|
|
|
return root;
|
|
}
|
|
|
|
private void EnsureActionButtons()
|
|
{
|
|
if (_buttonsPanel.Children.Count > 0)
|
|
return;
|
|
|
|
foreach (var action in _instance.Definition.Actions)
|
|
{
|
|
var btn = new Button
|
|
{
|
|
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)
|
|
};
|
|
btn.Click += (_, _) => Click(action.Index);
|
|
_buttonsPanel.Children.Add(btn);
|
|
}
|
|
|
|
if (_instance.Definition.CancelAction is not null || _instance.Definition.CanCloseWithEscape)
|
|
{
|
|
var cancel = new Button
|
|
{
|
|
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();
|
|
_buttonsPanel.Children.Add(cancel);
|
|
}
|
|
}
|
|
|
|
private Control BuildInput(DialogInput input)
|
|
{
|
|
_instance.Values.TryGetValue(input.Key, out var value);
|
|
value ??= input.InitialValue;
|
|
|
|
var panel = new StackPanel { Spacing = 0, Margin = new Thickness(0) };
|
|
|
|
if (input.LabelVisible && !string.IsNullOrWhiteSpace(input.Label))
|
|
panel.Children.Add(McColorParser.CreateColoredTextBlock(input.Label));
|
|
|
|
Control inner = input.Kind switch
|
|
{
|
|
DialogInputKind.Boolean => BuildBooleanInput(value, input),
|
|
DialogInputKind.SingleOption => BuildOptionInput(input, value),
|
|
DialogInputKind.NumberRange => BuildNumberInput(input, value),
|
|
_ => BuildTextInput(input, value)
|
|
};
|
|
|
|
_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 Control BuildTextInput(DialogInput input, string value)
|
|
{
|
|
var tb = new TextBox
|
|
{
|
|
Text = value,
|
|
AcceptsReturn = input.Multiline,
|
|
TextWrapping = input.Multiline ? TextWrapping.Wrap : TextWrapping.NoWrap,
|
|
MaxLength = input.MaxLength,
|
|
Foreground = Brushes.White,
|
|
Background = new SolidColorBrush(InputBg),
|
|
BorderThickness = new Thickness(0),
|
|
Padding = new Thickness(0)
|
|
};
|
|
|
|
return tb;
|
|
}
|
|
|
|
private Control BuildBooleanInput(string value, DialogInput input)
|
|
{
|
|
return new CheckBox
|
|
{
|
|
IsChecked = value.Equals("true", StringComparison.OrdinalIgnoreCase),
|
|
Foreground = Brushes.White,
|
|
Padding = new Thickness(0)
|
|
};
|
|
}
|
|
|
|
private Control BuildOptionInput(DialogInput input, string value)
|
|
{
|
|
var combo = new ComboBox
|
|
{
|
|
ItemsSource = input.Options ?? [],
|
|
Foreground = Brushes.White,
|
|
Background = new SolidColorBrush(InputBg),
|
|
BorderThickness = new Thickness(0),
|
|
Padding = new Thickness(1, 0)
|
|
};
|
|
|
|
combo.SelectedItem = input.Options?.FirstOrDefault(option => option.Id.Equals(value, StringComparison.Ordinal))
|
|
?? input.Options?.FirstOrDefault();
|
|
|
|
return combo;
|
|
}
|
|
|
|
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 = min,
|
|
Maximum = max,
|
|
Value = numValue,
|
|
TickFrequency = input.Step ?? 1,
|
|
IsSnapToTickEnabled = input.Step is not null,
|
|
Foreground = new SolidColorBrush(AccentColor)
|
|
};
|
|
|
|
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)
|
|
{
|
|
if (!StoreInputs())
|
|
return;
|
|
|
|
var result = _handler.Dialogs.Click(index);
|
|
SetStatus(result.Message);
|
|
if (result.Success)
|
|
CloseIfInactive();
|
|
}
|
|
|
|
private bool StoreInputs()
|
|
{
|
|
foreach (var input in _instance.Definition.Inputs)
|
|
{
|
|
if (!_inputControls.TryGetValue(input.Key, out var control))
|
|
continue;
|
|
|
|
var value = control switch
|
|
{
|
|
TextBox textBox => textBox.Text ?? string.Empty,
|
|
CheckBox checkBox => checkBox.IsChecked == true ? "true" : "false",
|
|
ComboBox comboBox when comboBox.SelectedItem is DialogOption option => option.Id,
|
|
Slider slider => NumberToString((float)slider.Value),
|
|
_ => input.InitialValue
|
|
};
|
|
|
|
var result = _handler.Dialogs.SetInput(input.Key, value);
|
|
if (!result.Success)
|
|
{
|
|
SetStatus(result.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
if (current is null)
|
|
{
|
|
DialogTuiHost.CloseCurrent();
|
|
return;
|
|
}
|
|
|
|
if (current.Revision != _instance.Revision)
|
|
DialogTuiHost.TryOpen(_handler, current, force: true);
|
|
}
|
|
|
|
private void SetStatus(string text)
|
|
{
|
|
_status.Text = text;
|
|
}
|
|
|
|
private void OnTunnelKeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
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)
|
|
{
|
|
var integer = (int)value;
|
|
return integer == value
|
|
? integer.ToString(CultureInfo.InvariantCulture)
|
|
: value.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
}
|