Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .codegraph/.gitignore
Original file line number Diff line number Diff line change
@@ -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
131 changes: 131 additions & 0 deletions PCL.Core/UI/Dialog.cs
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect non-blocking dialog requests

When callers pass block: false (for example LogService still uses MsgBoxWrapper.Show(..., false) for non-modal log popups), this assignment overwrites the requested value before Dialog_OnShow sees the context, so the UI bridge enters the blocking Dispatcher.PushFrame path 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 👍 / 👎.

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; }
}
21 changes: 18 additions & 3 deletions PCL.Core/UI/MsgBoxWrapper.cs
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,
Expand All @@ -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

View workflow job for this annotation

GitHub Actions / build (CI, ARM64) / Build

'MsgBoxButtonInfo' is obsolete: 'Use PCL.Core.UI.Dialog instead'

Check warning on line 27 in PCL.Core/UI/MsgBoxWrapper.cs

View workflow job for this annotation

GitHub Actions / build (CI, x64) / Build

'MsgBoxButtonInfo' is obsolete: 'Use PCL.Core.UI.Dialog instead'
MsgBoxTheme theme,

Check warning on line 28 in PCL.Core/UI/MsgBoxWrapper.cs

View workflow job for this annotation

GitHub Actions / build (CI, ARM64) / Build

'MsgBoxTheme' is obsolete: 'Use PCL.Core.UI.DialogTheme instead'

Check warning on line 28 in PCL.Core/UI/MsgBoxWrapper.cs

View workflow job for this annotation

GitHub Actions / build (CI, x64) / Build

'MsgBoxTheme' is obsolete: 'Use PCL.Core.UI.DialogTheme instead'
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

View workflow job for this annotation

GitHub Actions / build (CI, ARM64) / Build

The event 'MsgBoxWrapper.OnShow' is never used

Check warning on line 36 in PCL.Core/UI/MsgBoxWrapper.cs

View workflow job for this annotation

GitHub Actions / build (CI, x64) / Build

The event 'MsgBoxWrapper.OnShow' is never used

public static int ShowWithCustomButtons(
string message,
Expand All @@ -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));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve custom button result values

When legacy callers use MsgBoxWrapper.ShowWithCustomButtons with MsgBoxButtonInfo.Value values, this conversion drops the value and creates each DialogButton with the default id of 0. Dialog_OnShow then falls back to positional IDs, so selecting a button returns 1/2/3 instead of the caller-specified value; pass btn.Value into the DialogButton id to keep the existing result contract.

Useful? React with 👍 / 👎.

var result = Dialog.Show(new DialogContext

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve legacy MsgBoxWrapper handlers

When existing code subscribes to MsgBoxWrapper.OnShow, this wrapper path now bypasses that public event and calls Dialog.Show(...) directly, so legacy hosts/tests that still rely on the obsolete API never get a chance to render the dialog or set the ref result. Since the event remains exposed and FormMain still registers a handler for it, either bridge/invoke the legacy event here or make the old API fail explicitly instead of silently ignoring subscribers.

Useful? React with 👍 / 👎.

{
Caption = message,
Title = caption,
Theme = (DialogTheme)(int)theme,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): MsgBoxButtonInfo.Value 被丢弃了,可能会影响依赖自定义结果代码的调用方。

之前,ShowWithCustomButtons 会把每个 MsgBoxButtonInfo.Value 传递给 ref int result,但新的实现只把 Context/文本和 OnClick 带入 DialogButton / DialogContext.Result。依赖特定 Value 码的调用方现在只能看到位置索引(或者在从未设置时为 0)。如果这个 API 仍然需要保持向后兼容,就需要要么通过 DialogContext 传递 Value 并在选择时映射回去,要么在文档中明确说明会忽略 Value,且结果仅基于索引。

Original comment in English

issue (bug_risk): MsgBoxButtonInfo.Value is dropped, potentially breaking callers that depend on custom result codes.

Previously, ShowWithCustomButtons propagated each MsgBoxButtonInfo.Value into the ref int result, but the new implementation only carries Context/text and OnClick into DialogButton / DialogContext.Result. Callers that depended on specific Value codes will now see only positional indices (or 0 if never set). If this API is still expected to be backward-compatible, you’ll need to either pass Value through DialogContext and map it back on selection, or clearly document that Value is ignored and results are purely index-based.

Block = block,
Content = null,
Buttons = buttons,
});
return result;
}

Expand Down
193 changes: 193 additions & 0 deletions Plain Craft Launcher 2/Controls/Dialog/DialogControl.cs
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore initial focus to input prompts

DialogControl always focuses the first button during load. Since MyMsgBoxInput prompts are now built with this container, the text box no longer receives the caret as the old input dialog did, so users typing immediately into prompts such as instance/profile names type nowhere until they click the field. Let input dialogs request focus for their MyTextBox instead of unconditionally focusing the first button.

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);
}
}
}
Loading
Loading