diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 000000000..d20c0fe4b --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/PCL.Core/UI/MsgBoxWrapper.cs b/PCL.Core/UI/MsgBoxWrapper.cs index b22174ad7..3861e3761 100644 --- a/PCL.Core/UI/MsgBoxWrapper.cs +++ b/PCL.Core/UI/MsgBoxWrapper.cs @@ -39,7 +39,8 @@ public static int ShowWithCustomButtons( ICollection buttonCollection) { var result = 0; - if (buttonCollection.Count == 0) buttonCollection = [new MsgBoxButtonInfo(Lang.Text("Common.Action.Confirm"))]; + if (buttonCollection.Count == 0) + buttonCollection = [new MsgBoxButtonInfo(Lang.Text("Common.Action.Confirm"))]; OnShow?.Invoke(message, caption, buttonCollection, theme, block, ref result); return result; } diff --git a/Plain Craft Launcher 2/Controls/Dialog/DialogControl.cs b/Plain Craft Launcher 2/Controls/Dialog/DialogControl.cs new file mode 100644 index 000000000..fb2821a47 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/Dialog/DialogControl.cs @@ -0,0 +1,193 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Threading; +using PCL.Core.UI; +using PCL.Core.UI.Controls; + +namespace PCL; + +public partial class DialogControl +{ + private int _result; + private bool _exited; + private readonly int _uuid = ModBase.GetUuid(); + internal readonly List<(DialogButton Data, MyButton Control)> _buttons = []; + + public DispatcherFrame WaitFrame { get; } = new(true); + + public string Title + { + get => LabTitle.Text; + set => LabTitle.Text = value; + } + + public bool IsWarn { get; set; } + + private bool _showTitle = true; + internal bool ShowTitle + { + get => _showTitle; + set + { + _showTitle = value; + LabTitle.Visibility = value ? Visibility.Visible : Visibility.Collapsed; + ShapeLine.Visibility = value ? Visibility.Visible : Visibility.Collapsed; + } + } + + public UIElement? DialogContent + { + get => (UIElement?)ContentArea.Content; + set => ContentArea.Content = value; + } + + public int Result => _result; + + public DialogControl() + { + try + { + InitializeComponent(); + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + } + catch (Exception ex) + { + ModBase.Log(ex, "DialogControl 初始化失败", ModBase.LogLevel.Hint); + } + + Loaded += OnLoad; + } + + public MyButton AddButton(DialogButton button) + { + var btn = DialogButtonBuilder.Build(button, IsWarn); + var buttonId = button.Id > 0 ? button.Id : _buttons.Count + 1; + btn.Click += (_, _) => + { + if (_exited) return; + if (button.OnClick is not null) + button.OnClick(); + else + Close(buttonId); + }; + _buttons.Add((button, btn)); + PanBtn.Children.Add(btn); + + return btn; + } + + public event Action? OnClosed; + + private void OnLoad(object sender, RoutedEventArgs e) + { + try + { + if (IsWarn) + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); + if (_buttons.Count > 1 && _buttons[0].Control.ColorType != MyButton.ColorState.Red) + _buttons[0].Control.ColorType = MyButton.ColorState.Highlight; + if (_buttons.Count > 0) + _buttons[0].Control.Focus(); + else + PanBtn.Visibility = Visibility.Collapsed; + + Opacity = 0d; + ModAnimation.AniStart( + ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, + (IsWarn + ? new ModBase.MyColor(140d, 80d, 0d, 0d) + : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), + "PanMsgBackground Background"); + ModAnimation.AniStart( + new ModAnimation.AniData[] + { + ModAnimation.AaOpacity(this, 1d, 120, 60), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, + -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + -TransformRotate.Angle, 300, 60, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "DialogControl " + _uuid); + + ModBase.Log("[Dialog] " + LabTitle.Text); + } + catch (Exception ex) + { + ModBase.Log(ex, "DialogControl 加载失败", ModBase.LogLevel.Hint); + } + } + + public void Close(int result) + { + if (_exited) return; + _exited = true; + _result = result; + CloseInternal(); + } + + public void Close() + { + if (_exited) return; + _exited = true; + _result = -1; + CloseInternal(); + } + + private void CloseInternal() + { + try + { + WaitFrame.Continue = false; + } + catch + { + // ignore + } + + try + { + ComponentDispatcher.PopModal(); + } + catch + { + // ignore + } + + OnClosed?.Invoke(_result); + + ModAnimation.AniStart( + new ModAnimation.AniData[] + { + ModAnimation.AaCode(() => + { + var hasMore = (ModMain.frmMain?.PanMsg?.Children.Count ?? 0) > 1; + if (!hasMore) + ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, + BlurBorder.BackgroundProperty, + new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, + ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + }, 30), + ModAnimation.AaOpacity(this, -Opacity, 80, 20), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, + 150, 0, new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => ((Grid)Parent)?.Children.Remove(this), after: true) + }, "DialogControl " + _uuid); + } + + private void Drag(object sender, MouseButtonEventArgs e) + { + try + { + if (e.LeftButton == MouseButtonState.Pressed && e.GetPosition(ShapeLine).Y <= 2d) + ModMain.frmMain.DragMove(); + } + catch (Exception ex) + { + ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); + } + } +} diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml b/Plain Craft Launcher 2/Controls/Dialog/DialogControl.xaml similarity index 72% rename from Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml rename to Plain Craft Launcher 2/Controls/Dialog/DialogControl.xaml index 1ba30ccf8..967d094a8 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml +++ b/Plain Craft Launcher 2/Controls/Dialog/DialogControl.xaml @@ -1,4 +1,4 @@ - - + + HorizontalAlignment="Left" Name="LabTitle" Margin="7,-1,70,9" + VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False" + MouseLeftButtonDown="Drag" /> - - + - - - + Margin="150,0,8,0" Orientation="Horizontal" /> - \ No newline at end of file + diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml deleted file mode 100644 index f0a783de9..000000000 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs deleted file mode 100644 index b8dc7c782..000000000 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Interop; -using PCL.Core.UI.Controls; - -namespace PCL; - -public partial class MyMsgInput -{ - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); - - public MyMsgInput(ModMain.MyMsgBoxConverter converter) - { - try - { - InitializeComponent(); - AppendUniqueNameSuffix(Btn1); - AppendUniqueNameSuffix(Btn2); - myConverter = converter; - LabTitle.Text = converter.Title; - LabText.Text = converter.Text; - PanText.Visibility = string.IsNullOrEmpty(converter.Text) ? Visibility.Collapsed : Visibility.Visible; - TextArea.Text = (string)converter.Content; - TextArea.HintText = converter.HintText; - TextArea.ValidateRules = converter.ValidateRules; - ConfigurePrimaryButton(converter.Button1, converter.IsWarn); - ConfigureSecondaryButton(converter.Button2); - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - } - - catch (Exception ex) - { - ModBase.Log(ex, "输入弹窗初始化失败", ModBase.LogLevel.Hint); - } - - Loaded += Load; - } - - private void AppendUniqueNameSuffix(FrameworkElement element) - { - element.Name += ModBase.GetUuid(); - } - - private void ConfigurePrimaryButton(string text, bool isWarn) - { - Btn1.Text = text; - if (isWarn) - { - Btn1.ColorType = MyButton.ColorState.Red; - LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); - } - } - - private void ConfigureSecondaryButton(string text) - { - Btn2.Text = text; - Btn2.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible; - } - - private void Load(object sender, EventArgs e) - { - try - { - // UI 初始化 - if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) - Btn1.ColorType = MyButton.ColorState.Highlight; - TextArea.Focus(); - TextArea.SelectionStart = TextArea.Text.Length; - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log("[Control] 输入弹窗:" + LabTitle.Text); - } - - catch (Exception ex) - { - ModBase.Log(ex, "输入弹窗加载失败", ModBase.LogLevel.Hint); - } - } - - private void Close() - { - // 结束线程阻塞 - myConverter.WaitFrame.Continue = false; - ComponentDispatcher.PopModal(); - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); - } - - public void Btn1_Click(object sender, MouseButtonEventArgs e) - { - TextArea.Validate(); // #5773 - if (myConverter.IsExited || !TextArea.IsValidated) - return; - myConverter.IsExited = true; - myConverter.Result = TextArea.Text; - Close(); - } - - public void Btn2_Click(object sender, MouseButtonEventArgs e) - { - if (myConverter.IsExited) - return; - myConverter.IsExited = true; - myConverter.Result = null; - Close(); - } - - private void TextCaption_ValidateChanged(object sender, EventArgs e) - { - Btn1.IsEnabled = TextArea.IsValidated; - } - - private void Drag(object sender, MouseButtonEventArgs e) - { - try - { - if (e.LeftButton == MouseButtonState.Pressed) - if (e.GetPosition(ShapeLine).Y <= 2d) - ModMain.frmMain.DragMove(); - } - catch (Exception ex) - { - ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); - } - } -} diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml deleted file mode 100644 index d34fd682f..000000000 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs deleted file mode 100644 index b6bc2faaf..000000000 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Interop; -using PCL.Core.UI.Controls; - -namespace PCL; - -public partial class MyMsgMarkdown -{ - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); - - public MyMsgMarkdown(ModMain.MyMsgBoxConverter converter) - { - try - { - InitializeComponent(); - AppendUniqueNameSuffix(Btn1); - AppendUniqueNameSuffix(Btn2); - AppendUniqueNameSuffix(Btn3); - myConverter = converter; - LabTitle.Text = converter.Title; - LabCaption.Markdown = converter.Text; - DataContext = this; - ConfigurePrimaryButton(converter.Button1, converter.IsWarn); - ConfigureSecondaryButton(Btn2, converter.Button2); - ConfigureSecondaryButton(Btn3, converter.Button3); - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - } - - catch (Exception ex) - { - ModBase.Log(ex, "普通弹窗初始化失败", ModBase.LogLevel.Hint); - } - - Loaded += Load; - } - - private void AppendUniqueNameSuffix(FrameworkElement element) - { - element.Name += ModBase.GetUuid(); - } - - private void ConfigurePrimaryButton(string text, bool isWarn) - { - Btn1.Text = text; - if (isWarn) - { - Btn1.ColorType = MyButton.ColorState.Red; - LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); - } - } - - private static void ConfigureSecondaryButton(MyButton button, string text) - { - button.Text = text; - button.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible; - } - - private void Load(object sender, EventArgs e) - { - try - { - // UI 初始化 - if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) - Btn1.ColorType = MyButton.ColorState.Highlight; - Btn1.Focus(); - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Markdown); - } - - catch (Exception ex) - { - ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint); - } - } - - private void Close() - { - // 结束线程阻塞 - if (myConverter.ForceWait || !string.IsNullOrEmpty(myConverter.Button2)) - myConverter.WaitFrame.Continue = false; - ComponentDispatcher.PopModal(); - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); - } - - public void Btn1_Click(object sender, MouseButtonEventArgs e) - { - if (myConverter.IsExited) - return; - if (myConverter.Button1Action is not null) - { - myConverter.Button1Action(); - } - else - { - myConverter.IsExited = true; - myConverter.Result = 1; - Close(); - } - } - - public void Btn2_Click(object sender, MouseButtonEventArgs e) - { - if (myConverter.IsExited) - return; - if (myConverter.Button2Action is not null) - { - myConverter.Button2Action(); - } - else - { - myConverter.IsExited = true; - myConverter.Result = 2; - Close(); - } - } - - public void Btn3_Click(object sender, MouseButtonEventArgs e) - { - if (myConverter.IsExited) - return; - if (myConverter.Button3Action is not null) - { - myConverter.Button3Action(); - } - else - { - myConverter.IsExited = true; - myConverter.Result = 3; - Close(); - } - } - - private void Drag(object? sender = null, MouseButtonEventArgs? e = null) - { - try - { - if (e.LeftButton == MouseButtonState.Pressed) - if (e.GetPosition(ShapeLine).Y <= 2d) - ModMain.frmMain.DragMove(); - } - catch (Exception ex) - { - ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); - } - } -} diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs deleted file mode 100644 index e33749f8f..000000000 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System.Collections; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Interop; -using PCL.Core.UI.Controls; - -namespace PCL; - -public partial class MyMsgSelect -{ - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); - - private int selectedIndex = -1; - - public MyMsgSelect(ModMain.MyMsgBoxConverter converter) - { - try - { - InitializeComponent(); - AppendUniqueNameSuffix(Btn1); - AppendUniqueNameSuffix(Btn2); - myConverter = converter; - LabTitle.Text = converter.Title; - ConfigurePrimaryButton(converter.Button1, converter.IsWarn); - ConfigureSecondaryButton(converter.Button2); - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - InitializeSelectionList(converter.Content); - } - - catch (Exception ex) - { - ModBase.Log(ex, "选择弹窗初始化失败", ModBase.LogLevel.Hint); - } - - Loaded += Load; - Btn1.Click += Btn1_Click; - Btn2.Click += Btn2_Click; - LabTitle.MouseLeftButtonDown += Drag; - PanBorder.MouseLeftButtonDown += Drag; - } - - private void AppendUniqueNameSuffix(FrameworkElement element) - { - element.Name += ModBase.GetUuid(); - } - - private void ConfigurePrimaryButton(string text, bool isWarn) - { - Btn1.Text = text; - if (isWarn) - { - Btn1.ColorType = MyButton.ColorState.Red; - LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); - } - } - - private void ConfigureSecondaryButton(string text) - { - Btn2.Text = text; - Btn2.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible; - } - - private void InitializeSelectionList(object content) - { - // 添加选择控件 - Btn1.IsEnabled = false; - foreach (var rawContent in (IEnumerable)content) - { - // 1. Initialize and get the actual element - // Note: We use a new variable because 'foreach' variables are read-only - var selectionContent = MyVirtualizingElement.TryInit((FrameworkElement)rawContent); - - // 2. Interface casting and event subscription - if (selectionContent is IMyRadio selection) - { - PanSelection.Children.Add((UIElement)selection); - selection.Check += (sender, e) => OnChecked((IMyRadio)sender, e); - - // 3. Property configuration based on specific type - if (selection is MyListItem listItem) - { - listItem.Type = MyListItem.CheckType.RadioBox; - listItem.MinHeight = 24.0; - } - else if (selection is MyRadioBox radioBox) - { - radioBox.MinHeight = 24.0; - } - } - } - } - - private void Load(object sender, EventArgs e) - { - try - { - // UI 初始化 - if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) - Btn1.ColorType = MyButton.ColorState.Highlight; - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log("[Control] 选择弹窗:" + LabTitle.Text); - } - - catch (Exception ex) - { - ModBase.Log(ex, "选择弹窗加载失败", ModBase.LogLevel.Hint); - } - } - - private void Close() - { - // 结束线程阻塞 - myConverter.WaitFrame.Continue = false; - ComponentDispatcher.PopModal(); - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); - } - - public void Btn1_Click(object sender, MouseButtonEventArgs e) - { - if (myConverter.IsExited || selectedIndex == -1) - return; - myConverter.IsExited = true; - myConverter.Result = selectedIndex; - Close(); - } - - public void Btn2_Click(object sender, MouseButtonEventArgs e) - { - if (myConverter.IsExited) - return; - myConverter.IsExited = true; - myConverter.Result = null; - Close(); - } - - private void OnChecked(IMyRadio sender, EventArgs e) - { - Btn1.IsEnabled = true; - selectedIndex = PanSelection.Children.IndexOf((UIElement)sender); - } - - private void Drag(object sender, MouseButtonEventArgs e) - { - try - { - if (e.LeftButton == MouseButtonState.Pressed) - if (e.GetPosition(ShapeLine).Y <= 2d) - ModMain.frmMain.DragMove(); - } - catch (Exception ex) - { - ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); - } - } -} diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml deleted file mode 100644 index ed540acd2..000000000 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs deleted file mode 100644 index ece7fe122..000000000 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Interop; -using PCL.Core.UI.Controls; - -namespace PCL; - -public partial class MyMsgText -{ - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); - - public MyMsgText(ModMain.MyMsgBoxConverter converter) - { - try - { - InitializeComponent(); - AppendUniqueNameSuffix(Btn1); - AppendUniqueNameSuffix(Btn2); - AppendUniqueNameSuffix(Btn3); - myConverter = converter; - LabTitle.Text = converter.Title; - LabCaption.Text = converter.Text; - ConfigurePrimaryButton(converter.Button1, converter.IsWarn); - ConfigureSecondaryButton(Btn2, converter.Button2); - ConfigureSecondaryButton(Btn3, converter.Button3); - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - } - - catch (Exception ex) - { - ModBase.Log(ex, "普通弹窗初始化失败", ModBase.LogLevel.Hint); - } - - Loaded += Load; - } - - private void AppendUniqueNameSuffix(FrameworkElement element) - { - element.Name += ModBase.GetUuid(); - } - - private void ConfigurePrimaryButton(string text, bool isWarn) - { - Btn1.Text = text; - if (isWarn) - { - Btn1.ColorType = MyButton.ColorState.Red; - LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); - } - } - - private static void ConfigureSecondaryButton(MyButton button, string text) - { - button.Text = text; - button.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible; - } - - private void Load(object sender, RoutedEventArgs e) - { - try - { - // UI 初始化 - if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) - Btn1.ColorType = MyButton.ColorState.Highlight; - Btn1.Focus(); - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Text); - } - - catch (Exception ex) - { - ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint); - } - } - - private void Close() - { - // 结束线程阻塞 - if (myConverter.ForceWait || !string.IsNullOrEmpty(myConverter.Button2)) - myConverter.WaitFrame.Continue = false; - ComponentDispatcher.PopModal(); - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); - } - - public void Btn1_Click(object? sender = null, MouseButtonEventArgs? e = null) - { - if (myConverter.IsExited) - return; - if (myConverter.Button1Action is not null) - { - myConverter.Button1Action(); - } - else - { - myConverter.IsExited = true; - myConverter.Result = 1; - Close(); - } - } - - public void Btn2_Click(object sender, MouseButtonEventArgs e) - { - if (myConverter.IsExited) - return; - if (myConverter.Button2Action is not null) - { - myConverter.Button2Action(); - } - else - { - myConverter.IsExited = true; - myConverter.Result = 2; - Close(); - } - } - - public void Btn3_Click(object sender, MouseButtonEventArgs e) - { - if (myConverter.IsExited) - return; - if (myConverter.Button3Action is not null) - { - myConverter.Button3Action(); - } - else - { - myConverter.IsExited = true; - myConverter.Result = 3; - Close(); - } - } - - private void Drag(object sender, MouseButtonEventArgs e) - { - try - { - if (e.LeftButton == MouseButtonState.Pressed) - if (e.GetPosition(ShapeLine).Y <= 2d) - ModMain.frmMain.DragMove(); - } - catch (Exception ex) - { - ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); - } - } -} diff --git a/Plain Craft Launcher 2/Dialog/Dialog.cs b/Plain Craft Launcher 2/Dialog/Dialog.cs new file mode 100644 index 000000000..f9b9c503f --- /dev/null +++ b/Plain Craft Launcher 2/Dialog/Dialog.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using System.Windows; + +namespace PCL; + +public class Dialog where T : FrameworkElement, new() +{ + private readonly DialogControl _dialog; + + public T Content { get; } + + public Dialog(DialogControl dialog) + { + _dialog = dialog; + Content = new T(); + _dialog.DialogContent = Content; + } + + public Task GetResultAsync() + { + var tcs = new TaskCompletionSource(); + _dialog.OnClosed += result => tcs.TrySetResult(result); + return tcs.Task; + } + + public void Close(int result) => _dialog.Close(result); +} diff --git a/Plain Craft Launcher 2/Dialog/DialogButton.cs b/Plain Craft Launcher 2/Dialog/DialogButton.cs new file mode 100644 index 000000000..920fd29ed --- /dev/null +++ b/Plain Craft Launcher 2/Dialog/DialogButton.cs @@ -0,0 +1,34 @@ +using System; +using PCL.Core.App.Localization; + +namespace PCL.Core.UI; + +public class DialogButton +{ + public string Text { get; set; } + public int Id { get; set; } + public Action? OnClick { get; set; } + public bool IsPrimary { get; set; } + public bool IsCancel { get; set; } + + public DialogButton(string text, Action? onClick = null, bool isPrimary = false, int id = 0, bool isCancel = false) + { + Text = text; + OnClick = onClick; + IsPrimary = isPrimary; + Id = id; + IsCancel = isCancel; + } + + public static DialogButton Confirm(string? text = null) + => new(text ?? Lang.Text("Common.Action.Confirm"), isPrimary: true, id: DialogResult.Ok); + + public static DialogButton Cancel(string? text = null) + => new(text ?? Lang.Text("Common.Action.Cancel"), id: DialogResult.Cancel, isCancel: true); + + public static DialogButton Yes(string? text = null) + => new(text ?? Lang.Text("Common.Option.Yes"), isPrimary: true, id: DialogResult.Yes); + + public static DialogButton No(string? text = null) + => new(text ?? Lang.Text("Common.Option.No"), id: DialogResult.No, isCancel: true); +} diff --git a/Plain Craft Launcher 2/Dialog/DialogButtonBuilder.cs b/Plain Craft Launcher 2/Dialog/DialogButtonBuilder.cs new file mode 100644 index 000000000..6ef5c23c3 --- /dev/null +++ b/Plain Craft Launcher 2/Dialog/DialogButtonBuilder.cs @@ -0,0 +1,26 @@ +using System.Windows; +using PCL.Core.UI; + +namespace PCL; + +public static class DialogButtonBuilder +{ + public static MyButton Build(DialogButton button, bool isWarn) + { + var btn = new MyButton + { + Text = button.Text, + ColorType = button.IsPrimary + ? (isWarn ? MyButton.ColorState.Red : MyButton.ColorState.Highlight) + : MyButton.ColorState.Normal, + Visibility = string.IsNullOrEmpty(button.Text) ? Visibility.Collapsed : Visibility.Visible, + IsEnabled = true, + }; + btn.ApplyTemplate(); + btn.TextPadding = new Thickness(7); + btn.Padding = new Thickness(5, 0, 5, 0); + btn.Margin = new Thickness(12, 0, 0, 0); + btn.Name += ModBase.GetUuid(); + return btn; + } +} diff --git a/Plain Craft Launcher 2/Dialog/DialogContext.cs b/Plain Craft Launcher 2/Dialog/DialogContext.cs new file mode 100644 index 000000000..c3f2bde5d --- /dev/null +++ b/Plain Craft Launcher 2/Dialog/DialogContext.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.ObjectModel; + +namespace PCL.Core.UI; + +public class DialogContext +{ + public string Caption { get; set; } = ""; + public string Title { get; set; } = ""; + public DialogTheme Theme { get; set; } = DialogTheme.Info; + public bool Block { get; set; } = true; + public bool ShowTitle { get; set; } = true; + public object? Content { get; set; } + public Collection Buttons { get; set; } = []; + public int Result { get; set; } + public Action? OnClosed { get; set; } + internal DialogControl? _dialog; +} diff --git a/Plain Craft Launcher 2/Dialog/DialogManager.cs b/Plain Craft Launcher 2/Dialog/DialogManager.cs new file mode 100644 index 000000000..e1ec5c0fc --- /dev/null +++ b/Plain Craft Launcher 2/Dialog/DialogManager.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Interop; +using System.Windows.Threading; +using Microsoft.VisualBasic; +using PCL.Core.App.Localization; +using PCL.Core.UI; +using PCL.Core.Utils; + +namespace PCL; + +public class DialogManager +{ + public static DialogManager? Instance { get; private set; } + + private readonly FormMain _mainForm; + + public DialogManager(FormMain mainForm) + { + _mainForm = mainForm; + Instance = this; + } + + // -- new API -- + + public int Show(DialogContext context) + { + if (context.Buttons.Count == 0) + context.Buttons.Add(DialogButton.Confirm()); + context.Block = true; + ShowOnUi(context); + return context.Result; + } + + public Task ShowAsync(DialogContext context) + { + if (context.Buttons.Count == 0) + context.Buttons.Add(DialogButton.Confirm()); + var tcs = new TaskCompletionSource(); + context.Block = false; + context.OnClosed = result => tcs.TrySetResult(result); + ShowOnUi(context); + return tcs.Task; + } + + // -- lightweight: pure content, return handle -- + + public DialogControl Show(UIElement content, string? title = null, + DialogButton[]? buttons = null, bool isWarn = false, bool showTitle = true) + { + var ctx = new DialogContext + { + Title = title ?? "", + Theme = isWarn ? DialogTheme.Warning : DialogTheme.Info, + Block = false, + ShowTitle = showTitle && !string.IsNullOrEmpty(title), + Content = content, + Buttons = new Collection(), + }; + if (buttons is not null) + foreach (var b in buttons) ctx.Buttons.Add(b); + ShowOnUi(ctx); + return ctx._dialog!; + } + + // -- Show: typed content factory -- + + public Dialog Show(string? title = null, DialogButton[]? buttons = null, + bool isWarn = false, bool showTitle = true) where T : FrameworkElement, new() + { + var dialog = new DialogControl + { + Title = title ?? "", + IsWarn = isWarn, + ShowTitle = showTitle && !string.IsNullOrEmpty(title), + }; + var wrapper = new Dialog(dialog); + if (buttons is not null) + foreach (var b in buttons) dialog.AddButton(b); + _showContentOnUi(dialog); + return wrapper; + } + + // -- OAuth login (specialized) -- + + public object? ShowOAuth(JsonObject data, string authUrl) + { + object? result = null; + var ready = new ManualResetEventSlim(false); + + _mainForm.Dispatcher.Invoke(() => + { + var dialog = new DialogControl + { + IsWarn = false, + ShowTitle = true, + }; + _ = new DialogMsOAuthLogin(dialog, data, authUrl, r => + { + result = r; + ready.Set(); + }); + _mainForm.PanMsg.Children.Add(dialog); + }); + + ready.Wait(); + Thread.Sleep(200); + ModMain.frmMain!.ShowWindowToTop(); + return result; + } + + // -- convenience methods -- + + public int ShowText(string caption, string? title = null, + DialogTheme theme = DialogTheme.Info, bool block = true, + params string[] buttons) + { + return Show(new DialogContext + { + Caption = caption, + Title = title ?? Lang.Text("Common.Dialog.Title"), + Theme = theme, + Block = block, + Buttons = _BuildButtons(buttons), + }); + } + + public int ShowMarkdown(string markdown, string? title = null, + DialogTheme theme = DialogTheme.Info, bool block = true, + params string[] buttons) + { + var ctx = new DialogContext + { + Caption = markdown, + Title = title ?? Lang.Text("Common.Dialog.Title"), + Theme = theme, + Block = block, + Buttons = _BuildButtons(buttons), + Content = new Markdig.Wpf.MarkdownViewer { Markdown = markdown }, + }; + return Show(ctx); + } + + public string? ShowInput(string title, string text = "", string defaultInput = "", + Collection>? validateRules = null, + string hintText = "", string? button1 = null, string? button2 = null, bool isWarn = false) + { + button1 ??= Lang.Text("Common.Action.Confirm"); + button2 ??= Lang.Text("Common.Action.Cancel"); + string? result = null; + + Action showOnUi = () => + { + var stack = new StackPanel(); + if (!string.IsNullOrEmpty(text)) + { + stack.Children.Add(new TextBlock + { + Text = text, TextWrapping = TextWrapping.Wrap, FontSize = 15, + Margin = new Thickness(0, 0, 0, 7), + }); + } + var textBox = new MyTextBox + { + Text = defaultInput, HintText = hintText, + ValidateRules = validateRules ?? [], MinWidth = 450, + }; + stack.Children.Add(textBox); + + var dialog = new DialogControl { Title = title, IsWarn = isWarn, DialogContent = stack }; + dialog.AddButton(new DialogButton(button1!, onClick: () => + { + textBox.Validate(); + if (!textBox.IsValidated) return; + result = textBox.Text; + dialog.Close(1); + }, isPrimary: true, id: 1)); + if (!string.IsNullOrEmpty(button2)) + { + dialog.AddButton(new DialogButton(button2!, isCancel: true, onClick: () => + { + result = null; + dialog.Close(2); + }, id: 2)); + } + + _mainForm.PanMsg.Children.Add(dialog); + try + { + _mainForm.DragStop(); + ComponentDispatcher.PushModal(); + Dispatcher.PushFrame(dialog.WaitFrame); + } + finally + { + ComponentDispatcher.PopModal(); + } + }; + + try { _mainForm.Dispatcher.Invoke(showOnUi); } + catch (TaskCanceledException) { } + return result; + } + + public int? ShowSelect(List selections, string? title = null, + string? button1 = null, string? button2 = "", bool isWarn = false) + { + title ??= Lang.Text("Common.Dialog.Title"); + button1 ??= Lang.Text("Common.Action.Confirm"); + button2 ??= ""; + int? result = null; + + Action showOnUi = () => + { + var panel = new StackPanel(); + var dialog = new DialogControl { Title = title, IsWarn = isWarn, DialogContent = panel }; + + var b1 = dialog.AddButton(new DialogButton(button1!, onClick: () => { }, isPrimary: true, id: 1)); + b1.IsEnabled = false; + + if (!string.IsNullOrEmpty(button2)) + { + dialog.AddButton(new DialogButton(button2!, isCancel: true, onClick: () => + { + result = null; + dialog.Close(2); + }, id: 2)); + } + + int selectedIndex = -1; + for (var i = 0; i < selections.Count; i++) + { + var raw = selections[i]; + var item = MyVirtualizingElement.TryInit((FrameworkElement)raw); + if (item is IMyRadio radio) + { + if (item is MyListItem listItem) { listItem.Type = MyListItem.CheckType.RadioBox; listItem.MinHeight = 24; } + else if (item is MyRadioBox radioBox) { radioBox.MinHeight = 24; } + + var idx = i; + radio.Check += (_, _) => { selectedIndex = idx; b1.IsEnabled = true; }; + panel.Children.Add((UIElement)radio); + } + } + + b1.Click += (_, _) => + { + if (selectedIndex < 0) return; + result = selectedIndex; + dialog.Close(1); + }; + + _mainForm.PanMsg.Children.Add(dialog); + try + { + if (_mainForm is not null) _mainForm.DragStop(); + ComponentDispatcher.PushModal(); + Dispatcher.PushFrame(dialog.WaitFrame); + } + finally + { + ComponentDispatcher.PopModal(); + } + }; + + try { _mainForm.Dispatcher.Invoke(showOnUi); } + catch (TaskCanceledException) { } + return result; + } + + // -- legacy bridge: called by MsgBoxWrapper via ModMain.MsgBoxWrapper_OnShow -- + + public int ShowLegacy(string message, string caption, ICollection buttons, + MsgBoxTheme theme, bool block) + { + var ctx = new DialogContext + { + Caption = message, + Title = caption, + Theme = (DialogTheme)(int)theme, + Block = block, + Buttons = new Collection(), + }; + foreach (var btn in buttons) + ctx.Buttons.Add(new DialogButton(btn.Context, btn.OnClick)); + return Show(ctx); + } + + // -- tick: called from ModMain timer -- + + public void Tick() + { + try + { + if (_mainForm is null || _mainForm.PanMsg is null) return; + if (_mainForm.PanMsg.Children.Count > 0) + _mainForm.PanMsgBackground.Visibility = Visibility.Visible; + else if (_mainForm.PanMsgBackground.Visibility != Visibility.Collapsed) + _mainForm.PanMsgBackground.Visibility = Visibility.Collapsed; + } + catch (Exception ex) + { + ModBase.Log(ex, "对话框 Tick 失败", ModBase.LogLevel.Feedback); + } + } + + // -- internal show logic -- + + private void ShowOnUi(DialogContext context) + { + if (_mainForm is null || _mainForm.Dispatcher.HasShutdownStarted) + { + Interaction.MsgBox(context.Caption, MsgBoxStyle.OkOnly, context.Title); + context.Result = 1; + context.OnClosed?.Invoke(1); + return; + } + + if (_mainForm.Dispatcher.CheckAccess()) + ShowDialogOnUiThread(context); + else + try { _mainForm.Dispatcher.Invoke(() => ShowDialogOnUiThread(context)); } + catch (TaskCanceledException) { } + } + + private void ShowDialogOnUiThread(DialogContext context) + { + if (_mainForm?.PanMsg is null) return; + + var isWarn = context.Theme == DialogTheme.Warning || context.Theme == DialogTheme.Error; + + var content = context.Content; + if (content is null && !string.IsNullOrEmpty(context.Caption)) + { + content = new TextBlock + { + Text = context.Caption, + TextWrapping = TextWrapping.Wrap, + FontSize = 15, + }; + } + + var dialog = new DialogControl + { + Title = context.Title, + IsWarn = isWarn, + ShowTitle = context.ShowTitle, + DialogContent = content as UIElement, + }; + + for (var i = 0; i < context.Buttons.Count && i < 3; i++) + { + var btn = context.Buttons[i]; + if (i == 0) btn.IsPrimary = true; + if (btn.Id <= 0) btn.Id = i + 1; + dialog.AddButton(btn); + } + + context._dialog = dialog; + + _mainForm.PanMsg.Children.Add(dialog); + + if (context.Block) + { + try + { + _mainForm.DragStop(); + ComponentDispatcher.PushModal(); + Dispatcher.PushFrame(dialog.WaitFrame); + } + finally + { + ComponentDispatcher.PopModal(); + } + context.Result = dialog.Result; + context.OnClosed?.Invoke(context.Result); + } + else + { + dialog.OnClosed += result => + { + context.Result = result; + context.OnClosed?.Invoke(result); + }; + } + } + + private void _showContentOnUi(DialogControl dialog) + { + _mainForm.Dispatcher.Invoke(() => + { + _mainForm.PanMsg.Children.Add(dialog); + }); + } + + private static Collection _BuildButtons(string[] buttonTexts) + { + var list = new Collection(); + for (var i = 0; i < buttonTexts.Length; i++) + list.Add(new DialogButton(buttonTexts[i], isPrimary: i == 0, id: i + 1, + isCancel: buttonTexts.Length > 1 && i == buttonTexts.Length - 1)); + return list; + } +} diff --git a/Plain Craft Launcher 2/Dialog/DialogResult.cs b/Plain Craft Launcher 2/Dialog/DialogResult.cs new file mode 100644 index 000000000..f733f92d9 --- /dev/null +++ b/Plain Craft Launcher 2/Dialog/DialogResult.cs @@ -0,0 +1,9 @@ +namespace PCL.Core.UI; + +public static class DialogResult +{ + public const int Ok = 1; + public const int Cancel = 2; + public const int Yes = 6; + public const int No = 7; +} diff --git a/Plain Craft Launcher 2/Dialog/DialogTheme.cs b/Plain Craft Launcher 2/Dialog/DialogTheme.cs new file mode 100644 index 000000000..565c0f242 --- /dev/null +++ b/Plain Craft Launcher 2/Dialog/DialogTheme.cs @@ -0,0 +1,8 @@ +namespace PCL.Core.UI; + +public enum DialogTheme +{ + Info, + Warning, + Error +} diff --git a/Plain Craft Launcher 2/Dialog/LegacyDialog.cs b/Plain Craft Launcher 2/Dialog/LegacyDialog.cs new file mode 100644 index 000000000..180f0b2b5 --- /dev/null +++ b/Plain Craft Launcher 2/Dialog/LegacyDialog.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using FluentValidation; +using PCL.Core.App.Localization; +using PCL.Core.UI; + +namespace PCL; + +public static partial class ModMain +{ + #region Legacy wrappers — delegate to DialogManager + + public static int MyMsgBox(string caption, string? title = null, string? button1 = null, string? button2 = "", + string? button3 = "", bool isWarn = false, bool highLight = true, bool forceWait = false, + Action? button1Action = null, Action? button2Action = null, Action? button3Action = null) + { + var mgr = DialogManager.Instance; + if (mgr is null) return 1; + + var buttons = new List(); + if (!string.IsNullOrEmpty(button1)) buttons.Add(button1!); + if (!string.IsNullOrEmpty(button2)) buttons.Add(button2!); + if (!string.IsNullOrEmpty(button3)) buttons.Add(button3!); + if (buttons.Count == 0) buttons.Add(Lang.Text("Common.Action.Confirm")); + + return mgr.ShowText(caption, title, + isWarn ? DialogTheme.Warning : DialogTheme.Info, + forceWait || buttons.Count > 1, + buttons.ToArray()); + } + + public static int MyMsgBoxMarkdown(string caption, string? title = null, string? button1 = null, string? button2 = "", + string? button3 = "", bool isWarn = false, bool highLight = true, bool forceWait = false, + Action? button1Action = null, Action? button2Action = null, Action? button3Action = null) + { + var mgr = DialogManager.Instance; + if (mgr is null) return 1; + + var buttons = new List(); + if (!string.IsNullOrEmpty(button1)) buttons.Add(button1!); + if (!string.IsNullOrEmpty(button2)) buttons.Add(button2!); + if (!string.IsNullOrEmpty(button3)) buttons.Add(button3!); + if (buttons.Count == 0) buttons.Add(Lang.Text("Common.Action.Confirm")); + + return mgr.ShowMarkdown(caption, title, + isWarn ? DialogTheme.Warning : DialogTheme.Info, + forceWait || buttons.Count > 1, + buttons.ToArray()); + } + + public static string MyMsgBoxInput(string title, string text = "", string defaultInput = "", + Collection>? validateRules = null, string hintText = "", string? button1 = null, + string? button2 = null, bool isWarn = false) + { + return DialogManager.Instance?.ShowInput(title, text, defaultInput, + validateRules, hintText, button1, button2, isWarn) ?? ""; + } + + public static int? MyMsgBoxSelect(List selections, string? title = null, string? button1 = null, + string? button2 = "", bool isWarn = false) + { + return DialogManager.Instance?.ShowSelect(selections, title, button1, button2, isWarn); + } + + public static void MsgBoxWrapper_OnShow(string message, string caption, ICollection buttons, + MsgBoxTheme theme, bool block, ref int result) + { + result = DialogManager.Instance?.ShowLegacy(message, caption, buttons, theme, block) ?? 1; + } + + #endregion +} diff --git a/Plain Craft Launcher 2/FormMain.xaml.cs b/Plain Craft Launcher 2/FormMain.xaml.cs index 481c2395f..cb05bf7b6 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.cs +++ b/Plain Craft Launcher 2/FormMain.xaml.cs @@ -105,6 +105,8 @@ public FormMain() // 注册拖拽事件(不能直接加 Handles,否则没用;#6340) AddHandler(DragDrop.DragEnterEvent, new DragEventHandler(HandleDrag), true); AddHandler(DragDrop.DragOverEvent, new DragEventHandler(HandleDrag), true); + // 初始化 DialogManager + new DialogManager(this); // 注册 MsgBox 事件 MsgBoxWrapper.OnShow += ModMain.MsgBoxWrapper_OnShow; // 注册 Hint 事件 @@ -694,11 +696,12 @@ private void FormMain_KeyDown(object sender, KeyEventArgs e) var msg = PanMsg.Children[0]; Action? enterAction = msg switch { - MyMsgInput input => () => input.Btn1_Click(sender, null), - MyMsgSelect select => () => select.Btn1_Click(sender, null), - MyMsgText text => () => text.Btn1_Click(sender, null), - MyMsgMarkdown markdown => () => markdown.Btn1_Click(sender, null), - MyMsgLogin login => () => login.Btn1_Click(sender, null), + DialogControl dlg when dlg._buttons.Count > 0 => () => + { + var confirmEntry = dlg._buttons.FirstOrDefault(b => !b.Data.IsCancel); + var confirm = confirmEntry.Control ?? dlg._buttons[0].Control; + confirm.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); + }, _ => null }; enterAction?.Invoke(); @@ -710,25 +713,11 @@ private void FormMain_KeyDown(object sender, KeyEventArgs e) var msg = PanMsg.Children[0]; Action? escapeAction = msg switch { - MyMsgInput input => input.Btn2.Visibility == Visibility.Visible - ? () => input.Btn2_Click(sender, null) - : () => input.Btn1_Click(sender, null), - MyMsgSelect select => select.Btn2.Visibility == Visibility.Visible - ? () => select.Btn2_Click(sender, null) - : () => select.Btn1_Click(sender, null), - MyMsgText text => text.Btn3.Visibility == Visibility.Visible - ? () => text.Btn3_Click(sender, null) - : text.Btn2.Visibility == Visibility.Visible - ? () => text.Btn2_Click(sender, null) - : () => text.Btn1_Click(sender, null), - MyMsgMarkdown markdown => markdown.Btn3.Visibility == Visibility.Visible - ? () => markdown.Btn3_Click(sender, null) - : markdown.Btn2.Visibility == Visibility.Visible - ? () => markdown.Btn2_Click(sender, null) - : () => markdown.Btn1_Click(sender, null), - MyMsgLogin login => login.Btn3.Visibility == Visibility.Visible - ? () => login.Btn3_Click(sender, null) - : () => login.Btn1_Click(sender, null), + DialogControl dlg when dlg._buttons.Count > 0 => () => + { + var cancelEntry = dlg._buttons.FirstOrDefault(b => b.Data.IsCancel); + cancelEntry.Control?.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); + }, _ => null }; escapeAction?.Invoke(); @@ -787,7 +776,7 @@ private void FormMain_KeyDown(object sender, KeyEventArgs e) private void FormMain_MouseDown(object sender, MouseButtonEventArgs e) { // 鼠标侧键返回上一级 - if (ModMain.frmMain!.PanMsg.Children.Count > 0 || ModMain.WaitingMyMsgBox.Any()) + if (ModMain.frmMain!.PanMsg.Children.Count > 0) return; // 弹窗中(#5513) if (e.ChangedButton == MouseButton.XButton1 || e.ChangedButton == MouseButton.XButton2) TriggerPageBack(); diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.cs b/Plain Craft Launcher 2/Modules/Base/ModBase.cs index edd7f29fb..c6db67b03 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.cs +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.cs @@ -2425,20 +2425,13 @@ public static bool IsInstanceOfGenericType(this Type genericType, object obj) } private static int uuid = 1; - private static object uuidLock; /// /// 获取一个全程序内不会重复的数字(伪 Uuid)。 /// public static int GetUuid() { - if (uuidLock is null) - uuidLock = new object(); - lock (uuidLock) - { - uuid += 1; - return uuid; - } + return Interlocked.Increment(ref uuid); } /// diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs index 411d26dbc..2eabf0eda 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs @@ -890,13 +890,10 @@ private static string[] MsLoginStep1New(ModLoader.LoaderTask diff --git a/Plain Craft Launcher 2/Modules/ModMain.cs b/Plain Craft Launcher 2/Modules/ModMain.cs index 1e8d28c66..2472b45a4 100644 --- a/Plain Craft Launcher 2/Modules/ModMain.cs +++ b/Plain Craft Launcher 2/Modules/ModMain.cs @@ -21,7 +21,7 @@ namespace PCL; -public static class ModMain +public static partial class ModMain { public static FormMain? frmMain; public static SplashScreen? frmStart; @@ -104,11 +104,6 @@ private static ModBase.SafeList HintWaiting set; } - /// - /// 等待显示的弹窗。 - /// - public static List WaitingMyMsgBox { get; } = []; - private static void TimerMain() { try @@ -116,7 +111,7 @@ private static void TimerMain() #region 每 50ms 执行一次的代码 HintTick(); - MyMsgBoxTick(); + DialogManager.Instance?.Tick(); frmMain!.DragTick(); ModLoader.LoaderTaskbarProgressRefresh(); } @@ -473,441 +468,6 @@ private static void HideAllHint() #endregion - #region 弹窗 - - /// - /// 存储弹窗信息的转换器。 - /// - public class MyMsgBoxConverter - { - // 设置轮询 Url - public object AuthUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; - public string Button1 = ""; - - /// - /// 点击第一个按钮将执行该方法,不关闭弹窗。 - /// - public Action Button1Action; - - public string Button2 = ""; - - /// - /// 点击第二个按钮将执行该方法,不关闭弹窗。 - /// - public Action Button2Action; - - public string Button3 = ""; - - /// - /// 点击第三个按钮将执行该方法,不关闭弹窗。 - /// - public Action Button3Action; - - /// - /// 输入模式:文本框的文本。 - /// 选择模式:需要放进去的 List(Of MyListItem)。 - /// 登录模式:登录步骤 1 中返回的 JSON。 - /// - public object Content; - - public bool ForceWait; - - /// - /// 有多个按钮时,是否给第一个按钮加高亮。 - /// - public bool HighLight; - - /// - /// 输入模式:提示文本。 - /// - public string HintText = ""; - - /// - /// 弹窗是否已经关闭。 - /// - public bool IsExited = false; - - public bool IsWarn; - - /// - /// 输入模式:输入的文本。若点击了 非 第一个按钮,则为 Nothing。 - /// 选择模式:点击的按钮编号,从 1 开始。 - /// 登录模式:字符串数组 {AccessToken, RefreshToken} 或一个 Exception。 - /// - public object Result; - - public string Text; - public string Title; - public MyMsgBoxType Type; - - /// - /// 输入模式:输入验证规则。 - /// - public Collection> ValidateRules; - - public DispatcherFrame WaitFrame = new(true); - } - - public enum MyMsgBoxType - { - Text, - Select, - Input, - Login, - Markdown - } - - private static string GetDefaultDialogTitle() => Lang.Text("Common.Dialog.Title"); - - private static string GetDefaultConfirmText() => Lang.Text("Common.Action.Confirm"); - - private static string GetDefaultCancelText() => Lang.Text("Common.Action.Cancel"); - - /// - /// 显示弹窗,返回点击按钮的编号(从 1 开始)。 - /// - /// 弹窗的标题。 - /// 弹窗的内容。 - /// 显示的第一个按钮,默认为“确定”。 - /// 显示的第二个按钮,默认为空。 - /// 显示的第三个按钮,默认为空。 - /// 点击第一个按钮将执行该方法,不关闭弹窗。 - /// 点击第二个按钮将执行该方法,不关闭弹窗。 - /// 点击第三个按钮将执行该方法,不关闭弹窗。 - /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 - public static int MyMsgBox(string caption, string? title = null, string? button1 = null, string? button2 = "", - string? button3 = "", bool isWarn = false, bool highLight = true, bool forceWait = false, - Action button1Action = null, Action button2Action = null, Action button3Action = null) - { - title ??= GetDefaultDialogTitle(); - button1 ??= GetDefaultConfirmText(); - button2 ??= ""; - button3 ??= ""; - // 将弹窗列入队列 - var converter = new MyMsgBoxConverter - { - Type = MyMsgBoxType.Text, Button1 = button1, Button2 = button2, Button3 = button3, Text = caption, - IsWarn = isWarn, Title = title, HighLight = highLight, ForceWait = true, Button1Action = button1Action, - Button2Action = button2Action, Button3Action = button3Action - }; - WaitingMyMsgBox.Add(converter); - if (ModBase.RunInUi()) - // 若为 UI 线程,立即执行弹窗刻, 避免快速(连点器)点击时多次弹窗 - MyMsgBoxTick(); - if (button2.Length > 0 || forceWait) - { - // 若有多个按钮则开始等待 - if (frmMain is null || (frmMain.PanMsg is null && ModBase.RunInUi())) - { - // 主窗体尚未加载,用老土的弹窗来替代 - WaitingMyMsgBox.Remove(converter); - if (button2.Length > 0) - { - var rawResult = Interaction.MsgBox(caption, - (MsgBoxStyle)((int)(button3.Length > 0 ? MsgBoxStyle.YesNoCancel : MsgBoxStyle.YesNo) + - (int)(isWarn ? MsgBoxStyle.Critical : MsgBoxStyle.Question)), title); - switch (rawResult) - { - case MsgBoxResult.Yes: - { - converter.Result = 1; - break; - } - case MsgBoxResult.No: - { - converter.Result = 2; - break; - } - case MsgBoxResult.Cancel: - { - converter.Result = 3; - break; - } - } - } - else - { - Interaction.MsgBox(caption, - (MsgBoxStyle)((int)MsgBoxStyle.OkOnly + - (int)(isWarn ? MsgBoxStyle.Critical : MsgBoxStyle.Question)), title); - converter.Result = 1; - } - - ModBase.Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" + button1 + "," + button2 + "," + button3, - ModBase.LogLevel.Debug); - } - else - { - try - { - frmMain.DragStop(); - ComponentDispatcher.PushModal(); - Dispatcher.PushFrame(converter.WaitFrame); - } - finally - { - ComponentDispatcher.PopModal(); - } - } - - ModBase.Log($"[Control] 普通弹框返回:{converter.Result ?? "null"}"); - return (int)converter.Result; - } - - // 不进行等待,直接返回 - return 1; - } - - /// - /// 显示弹窗,返回点击按钮的编号(从 1 开始)。 - /// - /// 弹窗的标题。 - /// 弹窗的内容。 - /// 显示的第一个按钮,默认为“确定”。 - /// 显示的第二个按钮,默认为空。 - /// 显示的第三个按钮,默认为空。 - /// 点击第一个按钮将执行该方法,不关闭弹窗。 - /// 点击第二个按钮将执行该方法,不关闭弹窗。 - /// 点击第三个按钮将执行该方法,不关闭弹窗。 - /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 - public static int MyMsgBoxMarkdown(string caption, string? title = null, string? button1 = null, string? button2 = "", - string? button3 = "", bool isWarn = false, bool highLight = true, bool forceWait = false, - Action button1Action = null, Action button2Action = null, Action button3Action = null) - { - title ??= GetDefaultDialogTitle(); - button1 ??= GetDefaultConfirmText(); - button2 ??= ""; - button3 ??= ""; - // 将弹窗列入队列 - var converter = new MyMsgBoxConverter - { - Type = MyMsgBoxType.Markdown, Button1 = button1, Button2 = button2, Button3 = button3, Text = caption, - IsWarn = isWarn, Title = title, HighLight = highLight, ForceWait = true, Button1Action = button1Action, - Button2Action = button2Action, Button3Action = button3Action - }; - WaitingMyMsgBox.Add(converter); - if (ModBase.RunInUi()) - // 若为 UI 线程,立即执行弹窗刻, 避免快速(连点器)点击时多次弹窗 - MyMsgBoxTick(); - if (button2.Length > 0 || forceWait) - { - // 若有多个按钮则开始等待 - if (frmMain is null || (frmMain.PanMsg is null && ModBase.RunInUi())) - { - // 主窗体尚未加载,用老土的弹窗来替代 - WaitingMyMsgBox.Remove(converter); - if (button2.Length > 0) - { - var rawResult = Interaction.MsgBox(caption, - (MsgBoxStyle)((int)(button3.Length > 0 ? MsgBoxStyle.YesNoCancel : MsgBoxStyle.YesNo) + - (int)(isWarn ? MsgBoxStyle.Critical : MsgBoxStyle.Question)), title); - switch (rawResult) - { - case MsgBoxResult.Yes: - { - converter.Result = 1; - break; - } - case MsgBoxResult.No: - { - converter.Result = 2; - break; - } - case MsgBoxResult.Cancel: - { - converter.Result = 3; - break; - } - } - } - else - { - Interaction.MsgBox(caption, - (MsgBoxStyle)((int)MsgBoxStyle.OkOnly + - (int)(isWarn ? MsgBoxStyle.Critical : MsgBoxStyle.Question)), title); - converter.Result = 1; - } - - ModBase.Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" + button1 + "," + button2 + "," + button3, - ModBase.LogLevel.Debug); - } - else - { - try - { - frmMain.DragStop(); - ComponentDispatcher.PushModal(); - Dispatcher.PushFrame(converter.WaitFrame); - } - finally - { - ComponentDispatcher.PopModal(); - } - } - - ModBase.Log($"[Control] 普通弹框返回:{converter.Result ?? "null"}"); - return (int)converter.Result; - } - - // 不进行等待,直接返回 - return 1; - } - - /// - /// 显示输入框并返回输入的文本。若点击第二个按钮,则返回 Nothing。 - /// - /// 弹窗的标题。 - /// 文本框的输入检测。 - /// 弹窗的介绍文本。 - /// 文本框的默认内容。 - /// 文本框的提示内容。 - /// 显示的第一个按钮,默认为“确定”。 - /// 显示的第二个按钮,默认为“取消”。 - /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 - public static string MyMsgBoxInput(string title, string text = "", string defaultInput = "", - Collection>? validateRules = null, string hintText = "", string? button1 = null, - string? button2 = null, bool isWarn = false) - { - button1 ??= GetDefaultConfirmText(); - button2 ??= GetDefaultCancelText(); - // 将弹窗列入队列 - var converter = new MyMsgBoxConverter - { - Text = text, HintText = hintText, Type = MyMsgBoxType.Input, - ValidateRules = validateRules ?? [], Button1 = button1, Button2 = button2, - Content = defaultInput, IsWarn = isWarn, Title = title - }; - WaitingMyMsgBox.Add(converter); - // 虽然我也不知道这是啥但是能用就成了 :) - try - { - frmMain?.DragStop(); - ComponentDispatcher.PushModal(); - Dispatcher.PushFrame(converter.WaitFrame); - } - finally - { - ComponentDispatcher.PopModal(); - } - - ModBase.Log($"[Control] 输入弹框返回:{converter.Result}"); - return converter.Result?.ToString(); - } - - /// - /// 显示选择框并返回选择的第几项(从 0 开始)。若点击第二个按钮,则返回 Nothing。 - /// - /// 弹窗的标题。 - /// 显示的第一个按钮,默认为 “确定”。 - /// 显示的第二个按钮,默认为空。 - /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 - public static int? MyMsgBoxSelect(List selections, string? title = null, string? button1 = null, - string? button2 = "", bool isWarn = false) - { - title ??= GetDefaultDialogTitle(); - button1 ??= GetDefaultConfirmText(); - button2 ??= ""; - // 将弹窗列入队列 - var converter = new MyMsgBoxConverter - { - Type = MyMsgBoxType.Select, Button1 = button1, Button2 = button2, Content = selections, IsWarn = isWarn, - Title = title - }; - WaitingMyMsgBox.Add(converter); - // 虽然我也不知道这是啥但是能用就成了 :) - try - { - if (frmMain is not null) - frmMain.DragStop(); - ComponentDispatcher.PushModal(); - Dispatcher.PushFrame(converter.WaitFrame); - } - finally - { - ComponentDispatcher.PopModal(); - } - - ModBase.Log($"[Control] 选择弹框返回:{converter.Result ?? "null"}"); - return (int?)converter.Result; - } - - - public static void MyMsgBoxTick() - { - try - { - if (frmMain is null || frmMain.PanMsg is null || frmMain.WindowState == WindowState.Minimized) - return; - if (frmMain.PanMsg.Children.Count > 0) - { - // 弹窗中 - frmMain.PanMsgBackground.Visibility = Visibility.Visible; - } - else if (WaitingMyMsgBox.Any()) - { - // 没有弹窗,显示一个等待的弹窗 - frmMain.PanMsgBackground.Visibility = Visibility.Visible; - switch (WaitingMyMsgBox[0].Type) - { - case MyMsgBoxType.Input: - { - frmMain.PanMsg.Children.Add(new MyMsgInput(WaitingMyMsgBox[0])); - break; - } - case MyMsgBoxType.Select: - { - frmMain.PanMsg.Children.Add(new MyMsgSelect(WaitingMyMsgBox[0])); - break; - } - case MyMsgBoxType.Text: - { - frmMain.PanMsg.Children.Add(new MyMsgText(WaitingMyMsgBox[0])); - break; - } - case MyMsgBoxType.Login: - { - frmMain.PanMsg.Children.Add(new MyMsgLogin(WaitingMyMsgBox[0])); - break; - } - case MyMsgBoxType.Markdown: - { - frmMain.PanMsg.Children.Add(new MyMsgMarkdown(WaitingMyMsgBox[0])); - break; - } - } - - WaitingMyMsgBox.RemoveAt(0); - } - // 没有弹窗,没有等待的弹窗 - else if (!(frmMain.PanMsgBackground.Visibility == Visibility.Collapsed)) - { - frmMain.PanMsgBackground.Visibility = Visibility.Collapsed; - } - } - catch (Exception ex) - { - ModBase.Log(ex, "处理等待中的弹窗失败", ModBase.LogLevel.Feedback); - } - } - - public static void MsgBoxWrapper_OnShow(string message, string caption, ICollection buttons, - MsgBoxTheme theme, bool block, ref int result) - { - var btnText1 = buttons.Count < 1 ? GetDefaultConfirmText() : buttons.ElementAt(0).Context; - var btnAct1 = (Action)(buttons.Count < 1 ? (object)null : buttons.ElementAt(0).OnClick); - var btnText2 = buttons.Count < 2 ? GetDefaultCancelText() : buttons.ElementAt(1).Context; - var btnAct2 = (Action)(buttons.Count < 2 ? (object)null : buttons.ElementAt(1).OnClick); - var btnText3 = buttons.Count < 3 ? "" : buttons.ElementAt(2).Context; - var btnAct3 = (Action)(buttons.Count < 3 ? (object)null : buttons.ElementAt(2).OnClick); - - var isWarn = theme == MsgBoxTheme.Warning || theme == MsgBoxTheme.Error; - - result = MyMsgBox(message, caption, btnText1, btnText2, btnText3, isWarn, forceWait: block, - button1Action: btnAct1, button2Action: btnAct2, button3Action: btnAct3); - } - - #endregion - #region 愚人节 public static bool isAprilEnabled = DateTime.Now.Month == 4 && DateTime.Now.Day == 1; diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/DialogMsOAuthLogin.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/DialogMsOAuthLogin.xaml.cs new file mode 100644 index 000000000..58771c59c --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/DialogMsOAuthLogin.xaml.cs @@ -0,0 +1,179 @@ +using System; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using PCL.Core.App; +using PCL.Core.App.Localization; +using PCL.Core.UI; +using PCL.Core.Utils; +using PCL.Core.IO.Net.Http; + +namespace PCL; + +public class DialogMsOAuthLogin +{ + private readonly JsonObject _data; + private bool _finished; + private Task? _workingThread; + private TextBlock _caption = null!; + private MyButton _btnCancel = null!; + private string _website = ""; + private string _userCode = ""; + + public DialogMsOAuthLogin(DialogControl dialog, JsonObject data, string authUrl, Action onFinished) + { + _data = data; + + _userCode = (string)data["user_code"]!; + var deviceCode = (string)data["device_code"]!; + ModBase.ClipboardSet(deviceCode); + + string captionText; + if (data["verification_uri_complete"] is not null) + { + _website = (string)data["verification_uri_complete"]!; + captionText = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions.WithAutoFill", _userCode, _website); + } + else + { + _website = (string)data["verification_uri"]!; + captionText = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions", _userCode, _website); + } + + // Build content + _caption = new TextBlock + { + Text = captionText, + FontSize = 15, + LineHeight = 18, + TextWrapping = TextWrapping.Wrap, + FontWeight = FontWeights.Normal, + Padding = new Thickness(1), + }; + + var scrollViewer = new MyScrollViewer + { + Content = _caption, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + DeltaMult = 0.7, + }; + + dialog.Title = Lang.Text("Launch.Account.LoginDialog.MinecraftLogin"); + dialog.DialogContent = scrollViewer; + + // Buttons + var btnReopen = dialog.AddButton(new DialogButton( + Lang.Text("Launch.Account.LoginDialog.ReopenWebpage"), + onClick: () => ModBase.OpenWebsite(_website), id: 1)); + CustomEventService.SetEventData(btnReopen, _website); + + var btnCopy = dialog.AddButton(new DialogButton( + Lang.Text("Launch.Account.LoginDialog.CopyCode"), + onClick: () => ModBase.ClipboardSet(_userCode), id: 2)); + CustomEventService.SetEventData(btnCopy, _userCode); + + _btnCancel = dialog.AddButton(new DialogButton( + Lang.Text("Common.Action.Cancel"), isCancel: true, id: 3)); + + // Finish callback + dialog.OnClosed += result => + { + if (_finished) return; + _finished = true; + if (result == 3) + onFinished(new ThreadInterruptedException()); + }; + + _btnCancel.Click += (_, _) => + { + _finished = true; + onFinished(new ThreadInterruptedException()); + dialog.Close(3); + }; + + // Start OAuth polling + _workingThread = WorkThread(deviceCode, data, authUrl, onFinished, dialog); + } + + private async Task WorkThread(string deviceCode, JsonObject data, string oAuthUrl, + Action onFinished, DialogControl dialog) + { + try + { + await Task.Delay(2000).ConfigureAwait(false); + if (_finished) return; + + if (!string.IsNullOrEmpty(_website)) + ModBase.OpenWebsite(_website); + + ModBase.ClipboardSet(_userCode); + + var delayTime = (data["interval"]!.ToObject() - 1) * 1000; + var unknownFailureCount = 0; + + while (!_finished) + { + try + { + var bodyData = $"grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id={Secrets.MSOAuthClientId}&device_code={deviceCode}&scope=XboxLive.signin%20offline_access"; + using var resp = await HttpRequest + .Create("https://login.microsoftonline.com/consumers/oauth2/v2.0/token") + .WithFormContent(bodyData) + .SendAsync(enableLogging: false) + .ConfigureAwait(false); + + if (!resp.IsSuccess) + { + var error = await resp.AsJsonAsync().ConfigureAwait(false); + switch (error?.Error) + { + case "authorization_pending": + await Task.Delay(delayTime).ConfigureAwait(false); + continue; + default: + throw new Exception(error?.Error ?? "Unable to get body"); + } + } + + var ctx = await resp.AsStringAsync().ConfigureAwait(false); + var resultJson = (JsonObject)ModBase.GetJson(ctx); + ModProfile.ProfileLog($"令牌过期时间:{resultJson["expires_in"]} 秒"); + ModMain.Hint(Lang.Text("Launch.Account.LoginDialog.Success"), ModMain.HintType.Finish); + + _finished = true; + onFinished(new[] { resultJson["access_token"]!.ToString(), resultJson["refresh_token"]!.ToString() }); + dialog.Close(0); + return; + } + catch (Exception ex) + { + if (unknownFailureCount <= 2) + { + unknownFailureCount++; + ModBase.Log(ex, $"正版验证轮询第 {unknownFailureCount} 次失败"); + await Task.Delay(2000).ConfigureAwait(false); + } + else + { + _finished = true; + onFinished(new Exception(Lang.Text("Launch.Account.LoginDialog.PollingFailed"), ex)); + dialog.Close(-1); + return; + } + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, Lang.Text("Launch.Account.LoginDialog.Error.Init"), ModBase.LogLevel.Hint); + } + } + + private record ErrorBody( + [property: JsonPropertyName("error")] string Error, + [property: JsonPropertyName("error_description")] string Desc); +} diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml deleted file mode 100644 index f785c6484..000000000 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs deleted file mode 100644 index 6e5a6481b..000000000 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System.Windows.Controls; -using System.Windows.Input; -using PCL.Core.App; -using PCL.Core.App.Localization; -using PCL.Core.UI.Controls; -using PCL.Core.Utils; -using PCL.Core.IO.Net.Http; -using System.Text.Json.Serialization; - -namespace PCL; - -public partial class MyMsgLogin -{ - private readonly JsonObject data; - private string deviceCode; // 用于轮询的设备代码 - private string oAuthUrl = ""; // OAuth 轮询验证地址 - private string userCode; // 需要用户在网页上输入的设备代码 - private string website; // 验证网页的网址 - private Task? workingThread; - - public MyMsgLogin() - { - InitializeComponent(); - // Handles - Loaded += Load; - Btn1.Click += Btn1_Click; - Btn3.Click += Btn3_Click; - PanBorder.MouseLeftButtonDown += Drag; - LabTitle.MouseLeftButtonDown += Drag; - } - - private void Finished(object result) - { - if (myConverter.IsExited) - return; - myConverter.IsExited = true; - myConverter.Result = result; - ModBase.RunInUi(Close); - Thread.Sleep(200); - ModMain.frmMain.ShowWindowToTop(); - } - - private void Init() - { - userCode = (string)data["user_code"]; - deviceCode = (string)data["device_code"]; - ModBase.ClipboardSet(deviceCode); - if (data["verification_uri_complete"] is not null) - { - website = (string)data["verification_uri_complete"]; - LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions.WithAutoFill", userCode, website); - } - else - { - website = (string)data["verification_uri"]; - LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions", userCode, website); - } - - // 设置 UI - LabTitle.Text = Lang.Text("Launch.Account.LoginDialog.MinecraftLogin"); - CustomEventService.SetEventData(Btn1, website); - CustomEventService.SetEventData(Btn2, userCode); - // 启动工作线程 - workingThread = WorkThread(); - } - - private record ErrorBody( - [property: JsonPropertyName("error")] string Error, - [property: JsonPropertyName("error_description")] string Desc); - - private async Task WorkThread() - { - await Task.Delay(2000).ConfigureAwait(false); - if (myConverter.IsExited) - return; - ModBase.OpenWebsite(website); - ModBase.ClipboardSet(userCode); - var delayTime = (data["interval"].ToObject() - 1) * 1000; - // 轮询 - var unknownFailureCount = 0; - while (!myConverter.IsExited) - { - try - { - var bodyData = $"grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id={Secrets.MSOAuthClientId}&device_code={deviceCode}&scope=XboxLive.signin%20offline_access"; - using var result = await HttpRequest - .Create("https://login.microsoftonline.com/consumers/oauth2/v2.0/token") - .WithFormContent(bodyData) - .SendAsync(enableLogging: false) - .ConfigureAwait(false); - if (!result.IsSuccess) - { - var error = await result.AsJsonAsync() - .ConfigureAwait(false); - switch(error?.Error) - { - case "authorization_pending": - { - await Task.Delay(delayTime) - .ConfigureAwait(false); - continue; - } - default: - { - throw new Exception(error?.Error ?? "Unable to get body"); - } - } - } - // 获取结果 - var ctx = await result.AsStringAsync().ConfigureAwait(false); - var resultJson = (JsonObject)ModBase.GetJson(ctx); - ModProfile.ProfileLog($"令牌过期时间:{resultJson["expires_in"]} 秒"); - ModMain.Hint(Lang.Text("Launch.Account.LoginDialog.Success"), ModMain.HintType.Finish); - Finished(new[] { resultJson["access_token"].ToString(), resultJson["refresh_token"].ToString() }); - return; - } - catch (Exception ex) - { - if (unknownFailureCount <= 2) - { - unknownFailureCount += 1; - ModBase.Log(ex, $"正版验证轮询第 {unknownFailureCount} 次失败"); - ModBase.Log(ex.Message); - await Task.Delay(2000).ConfigureAwait(false); - } - else - { - Finished(new Exception(Lang.Text("Launch.Account.LoginDialog.PollingFailed"), ex)); - return; - } - } - } - } - - - #region 弹窗 - - private readonly ModMain.MyMsgBoxConverter myConverter; - private readonly int uuid = ModBase.GetUuid(); - - public MyMsgLogin(ModMain.MyMsgBoxConverter converter) - { - try - { - InitializeComponent(); - Btn1.Name += ModBase.GetUuid(); - Btn2.Name += ModBase.GetUuid(); - Btn3.Name += ModBase.GetUuid(); - myConverter = converter; - ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); - data = (JsonObject)converter.Content; - oAuthUrl = converter.AuthUrl?.ToString() ?? ""; - Init(); - } - catch (Exception ex) - { - ModBase.Log(ex, Lang.Text("Launch.Account.LoginDialog.Error.Init"), ModBase.LogLevel.Hint); - } - - Loaded += Load; - } - - private void Load(object sender, EventArgs e) - { - try - { - // 动画 - Opacity = 0d; - ModAnimation.AniStart( - ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty, - (myConverter.IsWarn - ? new ModBase.MyColor(140d, 80d, 0d, 0d) - : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200), - "PanMsgBackground Background"); - ModAnimation.AniStart( - new[] - { - ModAnimation.AaOpacity(this, 1d, 120, 60), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, - -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - -TransformRotate.Angle, 300, 60, - new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) - }, "MyMsgBox " + uuid); - // 记录日志 - ModBase.Log($"[Control] 正版验证弹窗:{LabTitle.Text}\r\n{LabCaption.Text}"); - } - catch (Exception ex) - { - ModBase.Log(ex, Lang.Text("Launch.Account.LoginDialog.Error.Load"), ModBase.LogLevel.Hint); - } - } - - private void Close() - { - // 动画 - ModAnimation.AniStart(new[] - { - ModAnimation.AaCode(() => - { - if (!ModMain.WaitingMyMsgBox.Any()) - ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, - BlurBorder.BackgroundProperty, - new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200, - ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); - }, 30), - ModAnimation.AaOpacity(this, -Opacity, 80, 20), - ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, - 150, 0, new ModAnimation.AniEaseOutFluent()), - ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, - 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), - ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true) - }, "MyMsgBox " + uuid); - } - - // 实现回车和 Esc 的接口(#4857) - public void Btn1_Click(object sender, MouseButtonEventArgs e) - { - } - - public void Btn3_Click(object sender, MouseButtonEventArgs e) - { - Finished(new ThreadInterruptedException()); - } - - private void Drag(object sender, MouseButtonEventArgs e) - { - // On Error Resume Next - if (e.GetPosition(ShapeLine).Y <= 2d) - ModMain.frmMain.DragMove(); - } - - #endregion -} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml b/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml index b72fb5bb0..9574ff481 100644 --- a/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml +++ b/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml @@ -23,6 +23,9 @@ Click="BtnCreateShortcut_Click" /> + diff --git a/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml.cs b/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml.cs index 902edbf37..e13b650ca 100644 --- a/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml.cs @@ -20,6 +20,7 @@ using PCL.Network.Loaders; using PCL.Core.App.Localization; using System.Globalization; +using WpfMedia = System.Windows.Media; namespace PCL; @@ -39,6 +40,7 @@ public PageToolsTest() Loaded += (_, _) => MeLoaded(); #if DEBUG BtnCrash.Visibility = Visibility.Visible; + BtnDialogTest.Visibility = Visibility.Visible; #endif } @@ -595,6 +597,92 @@ private void BtnCrash_Click(object sender, MouseButtonEventArgs e) throw new Exception(Lang.Text("Tools.Test.Crash.ManualCrash")); } + private void BtnDialogTest_Click(object sender, MouseButtonEventArgs e) + { + var panel = new StackPanel(); + + // Header + panel.Children.Add(new TextBlock + { + Text = "自定义弹窗示例", + FontSize = 18, + FontWeight = FontWeights.Bold, + Margin = new Thickness(0, 0, 0, 10), + }); + + // Info box + var infoBorder = new Border + { + BorderBrush = new WpfMedia.SolidColorBrush(WpfMedia.Color.FromRgb(74, 144, 217)), + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(6), + Padding = new Thickness(12), + Margin = new Thickness(0, 0, 0, 12), + }; + var infoStack = new StackPanel(); + infoStack.Children.Add(new TextBlock + { + Text = "✅ 此弹窗使用 DialogControl", + FontSize = 14, + Margin = new Thickness(0, 0, 0, 4), + }); + infoStack.Children.Add(new TextBlock + { + Text = "内容区可放置任意 FrameworkElement:\n文本、进度条、自定义控件等。\n按钮可绑定任意回调。", + TextWrapping = TextWrapping.Wrap, + FontSize = 13, + Foreground = WpfMedia.Brushes.Gray, + }); + infoBorder.Child = infoStack; + panel.Children.Add(infoBorder); + + // Progress + panel.Children.Add(new TextBlock + { + Text = "加载进度 70%", + FontSize = 13, + Margin = new Thickness(0, 0, 0, 4), + }); + panel.Children.Add(new ProgressBar + { + Value = 70, + Height = 6, + Margin = new Thickness(0, 0, 0, 12), + }); + + // Color swatches + var swatchBrushes = new[] { WpfMedia.Brushes.DodgerBlue, WpfMedia.Brushes.MediumSeaGreen, WpfMedia.Brushes.Tomato, WpfMedia.Brushes.Gold, WpfMedia.Brushes.MediumOrchid }; + var colorRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 8) }; + foreach (var brush in swatchBrushes) + { + colorRow.Children.Add(new System.Windows.Shapes.Rectangle + { + Width = 18, Height = 18, + Fill = brush, + RadiusX = 4, RadiusY = 4, + Margin = new Thickness(0, 0, 5, 0), + }); + } + panel.Children.Add(colorRow); + + // Hint + panel.Children.Add(new TextBlock + { + Text = "更多内容可由调用方自由定制。", + FontSize = 12, + Foreground = WpfMedia.Brushes.Gray, + Margin = new Thickness(0, 4, 0, 0), + }); + + DialogManager.Instance?.Show(new DialogContext + { + Title = "弹窗系统展示", + Content = panel, + Theme = DialogTheme.Info, + Buttons = [DialogButton.Confirm()], + }); + } + private int GetHeadSize() => CmbHeadSize.SelectedIndex switch { 0 => 64,