Files
PS_Launcher/src/PSLauncher.App/Views/ThemedMessageBox.xaml.cs
j.foucher 886245cc61 v0.16.0 — ThemedMessageBox replacing native MessageBox everywhere
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>
2026-05-03 10:46:53 +02:00

188 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
}