The Win32 MessageBox uses the system white-on-gray look that clashes with the launcher's dark theme (jarring transition every time we confirm a cancel, show an error, etc.). Replaced with a custom WPF dialog that: - Drops in : same Show(message, title, button, icon, default) API as System.Windows.MessageBox, returns the same MessageBoxResult. - Looks native to the launcher : dark Brush.Bg.Window background, Brush.Border outline, Brush.Text.Primary content, AccentButton for the default action and SecondaryButton for the others. - Iconography by emoji + colour code : ⛔ red (Error), ⚠ amber (Warning), ❓ neutral (Question), ℹ blue (Information). Mapped from MessageBoxImage. - Buttons localised via Strings.ActionOk / ActionYes / ActionNo / ActionCancel — works in fr/en/zh/th/ar like the rest of the UI. - Owner = currently active window (falls back to MainWindow then CenterScreen) so it positions correctly even from a child dialog. - Esc resolves to Cancel/No matching MessageBox semantics. The 21 MessageBox.Show call sites across MainViewModel, SettingsViewModel, LicenseDetailsDialog and MainWindow now use ThemedMessageBox.Show with no signature change — full grep replace + tiny `using` adjustments. Versions bumped to 0.16.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
188 lines
7.8 KiB
C#
188 lines
7.8 KiB
C#
using System.Windows;
|
||
using System.Windows.Input;
|
||
using System.Windows.Media;
|
||
using PSLauncher.Core.Localization;
|
||
|
||
namespace PSLauncher.App.Views;
|
||
|
||
/// <summary>
|
||
/// Drop-in pour <see cref="System.Windows.MessageBox"/> avec un look qui colle
|
||
/// au reste de la UI (dark theme, accent ASTERION). API copiée :
|
||
/// <code>
|
||
/// ThemedMessageBox.Show("Texte", "Titre", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||
/// </code>
|
||
///
|
||
/// Notes :
|
||
/// - <see cref="MessageBoxImage"/> est mappé vers une icône emoji + couleur.
|
||
/// - Les libellés des boutons (OK / Yes / No / Cancel) sont localisés via <see cref="Strings"/>.
|
||
/// - Owner = MainWindow par défaut (centrage), peut être passé explicitement.
|
||
/// - Esc renvoie Cancel ou No selon le bouton défini comme cancel.
|
||
/// </summary>
|
||
public partial class ThemedMessageBox : Window
|
||
{
|
||
public MessageBoxResult Result { get; private set; } = MessageBoxResult.None;
|
||
|
||
private MessageBoxResult _btn1Result = MessageBoxResult.None;
|
||
private MessageBoxResult _btn2Result = MessageBoxResult.None;
|
||
private MessageBoxResult _btn3Result = MessageBoxResult.None;
|
||
|
||
private ThemedMessageBox(string message, string title, MessageBoxButton button,
|
||
MessageBoxImage icon, MessageBoxResult defaultResult)
|
||
{
|
||
InitializeComponent();
|
||
|
||
Title = title;
|
||
TitleText.Text = title;
|
||
MessageText.Text = message;
|
||
SetIcon(icon);
|
||
SetupButtons(button, defaultResult);
|
||
|
||
// Esc = cancel/no
|
||
PreviewKeyDown += (_, e) =>
|
||
{
|
||
if (e.Key == Key.Escape)
|
||
{
|
||
Result = button switch
|
||
{
|
||
MessageBoxButton.OK => MessageBoxResult.OK,
|
||
MessageBoxButton.OKCancel => MessageBoxResult.Cancel,
|
||
MessageBoxButton.YesNo => MessageBoxResult.No,
|
||
MessageBoxButton.YesNoCancel => MessageBoxResult.Cancel,
|
||
_ => MessageBoxResult.None,
|
||
};
|
||
DialogResult = false;
|
||
Close();
|
||
}
|
||
};
|
||
}
|
||
|
||
private void SetIcon(MessageBoxImage icon)
|
||
{
|
||
// NOTE : MessageBoxImage.Stop ≡ Error (16) et Asterisk ≡ Information (64)
|
||
// côté enum .NET, donc on ne switche que sur les noms canoniques.
|
||
switch (icon)
|
||
{
|
||
case MessageBoxImage.Error: // = Stop = Hand
|
||
IconText.Text = "⛔";
|
||
IconText.Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
|
||
break;
|
||
case MessageBoxImage.Warning: // = Exclamation
|
||
IconText.Text = "⚠";
|
||
IconText.Foreground = new SolidColorBrush(Color.FromRgb(0xF5, 0x9E, 0x0B));
|
||
break;
|
||
case MessageBoxImage.Question:
|
||
IconText.Text = "❓";
|
||
break;
|
||
case MessageBoxImage.Information: // = Asterisk
|
||
IconText.Text = "ℹ";
|
||
IconText.Foreground = new SolidColorBrush(Color.FromRgb(0x3B, 0x82, 0xF6));
|
||
break;
|
||
default:
|
||
IconText.Visibility = Visibility.Collapsed;
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Configure jusqu'à 3 boutons. L'ordre visuel est gauche → droite, avec
|
||
/// le bouton "principal" (default) en accent à l'extrême droite. Les autres
|
||
/// en secondary à gauche.
|
||
/// </summary>
|
||
private void SetupButtons(MessageBoxButton button, MessageBoxResult defaultResult)
|
||
{
|
||
// Liste (label, result) dans l'ordre où on veut les afficher
|
||
var btns = button switch
|
||
{
|
||
MessageBoxButton.OK => new[] { (Strings.ActionOk, MessageBoxResult.OK) },
|
||
MessageBoxButton.OKCancel => new[] {
|
||
(Strings.ActionCancel, MessageBoxResult.Cancel),
|
||
(Strings.ActionOk, MessageBoxResult.OK),
|
||
},
|
||
MessageBoxButton.YesNo => new[] {
|
||
(Strings.ActionNo, MessageBoxResult.No),
|
||
(Strings.ActionYes, MessageBoxResult.Yes),
|
||
},
|
||
MessageBoxButton.YesNoCancel => new[] {
|
||
(Strings.ActionCancel, MessageBoxResult.Cancel),
|
||
(Strings.ActionNo, MessageBoxResult.No),
|
||
(Strings.ActionYes, MessageBoxResult.Yes),
|
||
},
|
||
_ => new[] { (Strings.ActionOk, MessageBoxResult.OK) },
|
||
};
|
||
|
||
// Btn3 = bouton accent (le plus à droite, principal). Toujours rempli.
|
||
// Btn2 = secondary à sa gauche, optionnel.
|
||
// Btn1 = secondary tout à gauche, optionnel.
|
||
// On remplit en partant de la fin pour que l'accent soit toujours sur Btn3.
|
||
var slots = new[] { Btn1, Btn2, Btn3 };
|
||
var resultSetters = new System.Action<MessageBoxResult>[]
|
||
{
|
||
r => _btn1Result = r,
|
||
r => _btn2Result = r,
|
||
r => _btn3Result = r,
|
||
};
|
||
|
||
int startSlot = 3 - btns.Length;
|
||
for (int i = 0; i < btns.Length; i++)
|
||
{
|
||
int slotIdx = startSlot + i;
|
||
slots[slotIdx].Content = btns[i].Item1;
|
||
slots[slotIdx].Visibility = Visibility.Visible;
|
||
resultSetters[slotIdx](btns[i].Item2);
|
||
// Default = celui dont le result match defaultResult
|
||
if (btns[i].Item2 == defaultResult)
|
||
slots[slotIdx].IsDefault = true;
|
||
}
|
||
|
||
// Si aucun match exact pour le default, on prend le bouton principal (Btn3)
|
||
if (Btn3.Visibility == Visibility.Visible && !Btn1.IsDefault && !Btn2.IsDefault && !Btn3.IsDefault)
|
||
Btn3.IsDefault = true;
|
||
}
|
||
|
||
private void OnBtn1Click(object sender, RoutedEventArgs e) => Finish(_btn1Result);
|
||
private void OnBtn2Click(object sender, RoutedEventArgs e) => Finish(_btn2Result);
|
||
private void OnBtn3Click(object sender, RoutedEventArgs e) => Finish(_btn3Result);
|
||
|
||
private void Finish(MessageBoxResult result)
|
||
{
|
||
Result = result;
|
||
DialogResult = true;
|
||
Close();
|
||
}
|
||
|
||
// ===================== STATIC SHOW OVERLOADS =====================
|
||
|
||
public static MessageBoxResult Show(string message)
|
||
=> Show(message, string.Empty, MessageBoxButton.OK, MessageBoxImage.None, MessageBoxResult.OK);
|
||
|
||
public static MessageBoxResult Show(string message, string title)
|
||
=> Show(message, title, MessageBoxButton.OK, MessageBoxImage.None, MessageBoxResult.OK);
|
||
|
||
public static MessageBoxResult Show(string message, string title, MessageBoxButton button)
|
||
=> Show(message, title, button, MessageBoxImage.None, DefaultFor(button));
|
||
|
||
public static MessageBoxResult Show(string message, string title, MessageBoxButton button, MessageBoxImage icon)
|
||
=> Show(message, title, button, icon, DefaultFor(button));
|
||
|
||
public static MessageBoxResult Show(string message, string title, MessageBoxButton button,
|
||
MessageBoxImage icon, MessageBoxResult defaultResult)
|
||
{
|
||
var owner = Application.Current?.Windows.OfType<Window>().FirstOrDefault(w => w.IsActive)
|
||
?? Application.Current?.MainWindow;
|
||
var dlg = new ThemedMessageBox(message, title, button, icon, defaultResult);
|
||
if (owner is not null && owner.IsVisible) dlg.Owner = owner;
|
||
else dlg.WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||
dlg.ShowDialog();
|
||
return dlg.Result;
|
||
}
|
||
|
||
private static MessageBoxResult DefaultFor(MessageBoxButton button) => button switch
|
||
{
|
||
MessageBoxButton.OK => MessageBoxResult.OK,
|
||
MessageBoxButton.OKCancel => MessageBoxResult.OK,
|
||
MessageBoxButton.YesNo => MessageBoxResult.Yes,
|
||
MessageBoxButton.YesNoCancel => MessageBoxResult.Yes,
|
||
_ => MessageBoxResult.None,
|
||
};
|
||
}
|