mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Dialog System First iteration, not tested
This commit is contained in:
parent
37cf19c175
commit
3d988c932f
14 changed files with 2250 additions and 20 deletions
343
MinecraftClient/Tui/DialogTuiHost.cs
Normal file
343
MinecraftClient/Tui/DialogTuiHost.cs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
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 readonly McClient _handler;
|
||||
private readonly DialogInstance _instance;
|
||||
private readonly TextBlock _status;
|
||||
private readonly Dictionary<string, Control> _inputControls = new(StringComparer.Ordinal);
|
||||
|
||||
public DialogView(McClient handler, DialogInstance instance)
|
||||
{
|
||||
_handler = handler;
|
||||
_instance = instance;
|
||||
|
||||
BorderBrush = Brushes.White;
|
||||
BorderThickness = new Thickness(1);
|
||||
Background = Brushes.Black;
|
||||
Padding = new Thickness(1);
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalAlignment = VerticalAlignment.Stretch;
|
||||
Focusable = true;
|
||||
|
||||
_status = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.Gray,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(1, 0)
|
||||
};
|
||||
|
||||
Child = BuildContent();
|
||||
|
||||
AttachedToVisualTree += (_, _) =>
|
||||
{
|
||||
AddHandler(KeyDownEvent, OnTunnelKeyDown, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||
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 main = new StackPanel
|
||||
{
|
||||
Spacing = 1,
|
||||
Margin = new Thickness(1)
|
||||
};
|
||||
|
||||
main.Children.Add(new TextBlock
|
||||
{
|
||||
Text = _instance.Definition.DisplayTitle(),
|
||||
Foreground = Brushes.Yellow,
|
||||
FontWeight = FontWeight.Bold,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
|
||||
foreach (var body in _instance.Definition.Body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body.Text))
|
||||
continue;
|
||||
|
||||
main.Children.Add(new TextBlock
|
||||
{
|
||||
Text = body.Text,
|
||||
Foreground = Brushes.White,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var input in _instance.Definition.Inputs)
|
||||
main.Children.Add(BuildInput(input));
|
||||
|
||||
var buttons = new WrapPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal
|
||||
};
|
||||
|
||||
foreach (var action in _instance.Definition.Actions)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Content = action.Label,
|
||||
Margin = new Thickness(0, 0, 1, 1),
|
||||
MinWidth = Math.Max(8, Math.Min(action.Label.Length + 4, 32))
|
||||
};
|
||||
button.Click += (_, _) => Click(action.Index);
|
||||
buttons.Children.Add(button);
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
cancel.Click += (_, _) => TryCloseByUser();
|
||||
buttons.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)
|
||||
{
|
||||
_instance.Values.TryGetValue(input.Key, out var value);
|
||||
value ??= input.InitialValue;
|
||||
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
Margin = new Thickness(0, 1)
|
||||
};
|
||||
|
||||
if (input.LabelVisible && !string.IsNullOrWhiteSpace(input.Label))
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = input.Label,
|
||||
Foreground = Brushes.LightGray,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
}
|
||||
|
||||
Control control = input.Kind switch
|
||||
{
|
||||
DialogInputKind.Boolean => BuildBooleanInput(value),
|
||||
DialogInputKind.SingleOption => BuildOptionInput(input, value),
|
||||
DialogInputKind.NumberRange => BuildNumberInput(input, value),
|
||||
_ => BuildTextInput(input, value)
|
||||
};
|
||||
|
||||
_inputControls[input.Key] = control;
|
||||
panel.Children.Add(control);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static Control BuildTextInput(DialogInput input, string value)
|
||||
{
|
||||
return new TextBox
|
||||
{
|
||||
Text = value,
|
||||
AcceptsReturn = input.Multiline,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxLength = input.MaxLength,
|
||||
Foreground = Brushes.White,
|
||||
Background = Brushes.Black,
|
||||
BorderBrush = Brushes.Gray
|
||||
};
|
||||
}
|
||||
|
||||
private static Control BuildBooleanInput(string value)
|
||||
{
|
||||
return new CheckBox
|
||||
{
|
||||
IsChecked = value.Equals("true", StringComparison.OrdinalIgnoreCase),
|
||||
Foreground = Brushes.White
|
||||
};
|
||||
}
|
||||
|
||||
private static Control BuildOptionInput(DialogInput input, string value)
|
||||
{
|
||||
var combo = new ComboBox
|
||||
{
|
||||
ItemsSource = input.Options ?? [],
|
||||
Foreground = Brushes.White
|
||||
};
|
||||
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)
|
||||
{
|
||||
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,
|
||||
TickFrequency = input.Step ?? 1,
|
||||
IsSnapToTickEnabled = input.Step is not null
|
||||
};
|
||||
return slider;
|
||||
}
|
||||
|
||||
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 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)
|
||||
return;
|
||||
|
||||
TryCloseByUser();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static string NumberToString(float value)
|
||||
{
|
||||
var integer = (int)value;
|
||||
return integer == value
|
||||
? integer.ToString(CultureInfo.InvariantCulture)
|
||||
: value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
|
@ -1207,6 +1207,15 @@ namespace MinecraftClient.Tui
|
|||
{
|
||||
if (e.Key == Key.Escape && _overlayContent != null)
|
||||
{
|
||||
if (_overlayContent is IOverlayCloseHandler closeHandler)
|
||||
{
|
||||
if (closeHandler.TryCloseByUser())
|
||||
HideOverlay();
|
||||
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
HideOverlay();
|
||||
e.Handled = true;
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue