-
Notifications
You must be signed in to change notification settings - Fork 113
refactor(msgbox): add customizable DialogControl + Dialog API #3168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 6 commits
bef259d
bf89224
a81caa7
afb6f74
1bd383f
e38a2c5
072e641
f7ad35d
be51f26
3dec522
9e4d841
99a6acb
024de3a
17f6761
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Collections.ObjectModel; | ||
| using System.Threading.Tasks; | ||
| using PCL.Core.App.Localization; | ||
|
|
||
| namespace PCL.Core.UI; | ||
|
|
||
| /// <summary> | ||
| /// Standard dialog result IDs for button matching. | ||
| /// </summary> | ||
| public static class DialogResult | ||
| { | ||
| public const int Ok = 1; | ||
| public const int Cancel = 2; | ||
| public const int Yes = 6; | ||
| public const int No = 7; | ||
| } | ||
|
|
||
| public class DialogButton | ||
| { | ||
| public string Text { get; set; } | ||
| public int Id { get; set; } | ||
| public Action? OnClick { get; set; } | ||
| public bool IsPrimary { get; set; } | ||
|
|
||
| public DialogButton(string text, Action? onClick = null, bool isPrimary = false, int id = 0) | ||
| { | ||
| Text = text; | ||
| OnClick = onClick; | ||
| IsPrimary = isPrimary; | ||
| Id = id; | ||
| } | ||
|
|
||
| // -- presets -- | ||
|
|
||
| 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); | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| public static class Dialog | ||
| { | ||
| public static event Action<DialogContext>? OnShow; | ||
|
|
||
| public static int Show(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, | ||
| Content = null, | ||
| Buttons = _BuildButtons(buttons), | ||
| }); | ||
| } | ||
|
|
||
| public static int Show(DialogContext context) | ||
| { | ||
| if (context.Buttons.Count == 0) | ||
| context.Buttons.Add(DialogButton.Confirm()); | ||
| context.Block = true; | ||
| context.OnClosed = null; | ||
| OnShow?.Invoke(context); | ||
| return context.Result; | ||
| } | ||
|
|
||
| public static Task<int> ShowAsync(DialogContext context) | ||
| { | ||
| if (context.Buttons.Count == 0) | ||
| context.Buttons.Add(DialogButton.Confirm()); | ||
| var tcs = new TaskCompletionSource<int>(); | ||
| context.Block = false; | ||
| context.OnClosed = result => tcs.TrySetResult(result); | ||
| OnShow?.Invoke(context); | ||
| return tcs.Task; | ||
| } | ||
|
|
||
| public static Task<int> ShowAsync(string caption, string? title = null, | ||
| DialogTheme theme = DialogTheme.Info, params string[] buttons) | ||
| { | ||
| return ShowAsync(new DialogContext | ||
| { | ||
| Caption = caption, | ||
| Title = title ?? Lang.Text("Common.Dialog.Title"), | ||
| Theme = theme, | ||
| Content = null, | ||
| Buttons = _BuildButtons(buttons), | ||
| }); | ||
| } | ||
|
|
||
| private static Collection<DialogButton> _BuildButtons(string[] buttonTexts) | ||
| { | ||
| var list = new Collection<DialogButton>(); | ||
| for (var i = 0; i < buttonTexts.Length; i++) | ||
| { | ||
| list.Add(new DialogButton(buttonTexts[i], isPrimary: i == 0, id: i + 1)); | ||
| } | ||
| return list; | ||
| } | ||
| } | ||
|
|
||
| public enum DialogTheme | ||
| { | ||
| Info, | ||
| Warning, | ||
| Error | ||
| } | ||
|
|
||
| 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 object? Content { get; set; } | ||
| public Collection<DialogButton> Buttons { get; set; } = []; | ||
| public int Result { get; set; } | ||
| public Action<int>? OnClosed { get; set; } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,19 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Collections.ObjectModel; | ||
| using System.Linq; | ||
| using PCL.Core.App.Localization; | ||
|
|
||
| namespace PCL.Core.UI; | ||
|
|
||
| [Obsolete("Use PCL.Core.UI.Dialog instead")] | ||
| public record MsgBoxButtonInfo( | ||
| string Context, | ||
| int Value = 0, | ||
| Action? OnClick = null | ||
| ); | ||
|
|
||
| [Obsolete("Use PCL.Core.UI.DialogTheme instead")] | ||
| public enum MsgBoxTheme | ||
| { | ||
| Info, | ||
|
|
@@ -21,15 +24,16 @@ | |
| public delegate void MsgBoxHandler( | ||
| string message, | ||
| string caption, | ||
| ICollection<MsgBoxButtonInfo> buttons, | ||
|
Check warning on line 27 in PCL.Core/UI/MsgBoxWrapper.cs
|
||
| MsgBoxTheme theme, | ||
|
Check warning on line 28 in PCL.Core/UI/MsgBoxWrapper.cs
|
||
| bool block, | ||
| ref int result | ||
| ); | ||
|
|
||
| [Obsolete("Use PCL.Core.UI.Dialog instead")] | ||
| public static class MsgBoxWrapper | ||
| { | ||
| public static event MsgBoxHandler? OnShow; | ||
|
Check warning on line 36 in PCL.Core/UI/MsgBoxWrapper.cs
|
||
|
|
||
| public static int ShowWithCustomButtons( | ||
| string message, | ||
|
|
@@ -38,9 +42,20 @@ | |
| bool block, | ||
| ICollection<MsgBoxButtonInfo> buttonCollection) | ||
| { | ||
| var result = 0; | ||
| if (buttonCollection.Count == 0) buttonCollection = [new MsgBoxButtonInfo(Lang.Text("Common.Action.Confirm"))]; | ||
| OnShow?.Invoke(message, caption, buttonCollection, theme, block, ref result); | ||
| var buttons = new Collection<DialogButton>(); | ||
| foreach (var btn in buttonCollection) | ||
| { | ||
| buttons.Add(new DialogButton(btn.Context, btn.OnClick)); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When legacy callers use Useful? React with 👍 / 👎. |
||
| var result = Dialog.Show(new DialogContext | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When existing code subscribes to Useful? React with 👍 / 👎. |
||
| { | ||
| Caption = message, | ||
| Title = caption, | ||
| Theme = (DialogTheme)(int)theme, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): MsgBoxButtonInfo.Value 被丢弃了,可能会影响依赖自定义结果代码的调用方。 之前, Original comment in Englishissue (bug_risk): MsgBoxButtonInfo.Value is dropped, potentially breaking callers that depend on custom result codes. Previously, |
||
| Block = block, | ||
| Content = null, | ||
| Buttons = buttons, | ||
| }); | ||
| return result; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.Controls; | ||
|
|
||
| namespace PCL; | ||
|
|
||
| public partial class DialogControl | ||
| { | ||
| private int _result; | ||
| private bool _exited; | ||
| private readonly int _uuid = ModBase.GetUuid(); | ||
| private readonly List<MyButton> _buttons = []; | ||
|
|
||
| public DispatcherFrame WaitFrame { get; } = new(true); | ||
|
|
||
| public string Title | ||
| { | ||
| get => LabTitle.Text; | ||
| set => LabTitle.Text = value; | ||
| } | ||
|
|
||
| public bool IsWarn { get; set; } | ||
|
|
||
| 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(string text, Action? onClick = null, bool isPrimary = false, int id = 0) | ||
| { | ||
| var btn = new MyButton | ||
| { | ||
| Text = text, | ||
| ColorType = isPrimary | ||
| ? (IsWarn ? MyButton.ColorState.Red : MyButton.ColorState.Highlight) | ||
| : MyButton.ColorState.Normal, | ||
| Visibility = string.IsNullOrEmpty(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(); | ||
| var buttonId = id > 0 ? id : _buttons.Count + 1; | ||
| btn.Click += (_, _) => | ||
| { | ||
| if (_exited) return; | ||
| if (onClick is not null) | ||
| { | ||
| onClick(); | ||
| } | ||
| else | ||
| { | ||
| Close(buttonId); | ||
| } | ||
| }; | ||
| _buttons.Add(btn); | ||
| PanBtn.Children.Add(btn); | ||
| return btn; | ||
| } | ||
|
|
||
| public event Action<int>? OnClosed; | ||
|
|
||
| private void OnLoad(object sender, RoutedEventArgs e) | ||
| { | ||
| try | ||
| { | ||
| if (_buttons.Count > 1 && _buttons[0].ColorType != MyButton.ColorState.Red) | ||
| _buttons[0].ColorType = MyButton.ColorState.Highlight; | ||
| if (_buttons.Count > 0) | ||
| _buttons[0].Focus(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
|
|
||
| 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.WaitingMyMsgBox.Any() | ||
| || (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); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When callers pass
block: false(for exampleLogServicestill usesMsgBoxWrapper.Show(..., false)for non-modal log popups), this assignment overwrites the requested value beforeDialog_OnShowsees the context, so the UI bridge enters the blockingDispatcher.PushFramepath anyway. This regresses the old fire-and-forget message boxes by making logging/background paths wait for user dismissal instead of just queuing the dialog.Useful? React with 👍 / 👎.