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>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#define MyAppName "PROSERVE Launcher"
|
#define MyAppName "PROSERVE Launcher"
|
||||||
#define MyAppShortName "PS_Launcher"
|
#define MyAppShortName "PS_Launcher"
|
||||||
#define MyAppVersion "0.15.0"
|
#define MyAppVersion "0.16.0"
|
||||||
#define MyAppPublisher "ASTERION VR"
|
#define MyAppPublisher "ASTERION VR"
|
||||||
#define MyAppURL "https://asterionvr.com"
|
#define MyAppURL "https://asterionvr.com"
|
||||||
#define MyAppExeName "PS_Launcher.exe"
|
#define MyAppExeName "PS_Launcher.exe"
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<Product>PROSERVE Launcher</Product>
|
<Product>PROSERVE Launcher</Product>
|
||||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||||
<Version>0.15.0</Version>
|
<Version>0.16.0</Version>
|
||||||
<AssemblyVersion>0.15.0.0</AssemblyVersion>
|
<AssemblyVersion>0.16.0.0</AssemblyVersion>
|
||||||
<FileVersion>0.15.0.0</FileVersion>
|
<FileVersion>0.16.0.0</FileVersion>
|
||||||
|
|
||||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Self-update failed");
|
_logger.LogError(ex, "Self-update failed");
|
||||||
MessageBox.Show(Strings.MsgSelfUpdateFailed(ex.Message),
|
ThemedMessageBox.Show(Strings.MsgSelfUpdateFailed(ex.Message),
|
||||||
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
|
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
StatusMessage = Strings.StatusSelfUpdateError(ex.Message);
|
StatusMessage = Strings.StatusSelfUpdateError(ex.Message);
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
@@ -518,7 +518,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
private void CancelDownload()
|
private void CancelDownload()
|
||||||
{
|
{
|
||||||
if (_activeDownloadCts is null || _activeRow is null) return;
|
if (_activeDownloadCts is null || _activeRow is null) return;
|
||||||
var confirm = MessageBox.Show(
|
var confirm = ThemedMessageBox.Show(
|
||||||
Strings.MsgCancelDownloadConfirm(_activeRow.Version),
|
Strings.MsgCancelDownloadConfirm(_activeRow.Version),
|
||||||
Strings.MsgBoxCancelDl,
|
Strings.MsgBoxCancelDl,
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||||||
@@ -535,7 +535,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
// Si un DL est actif sur cette même row, on doit d'abord l'annuler proprement.
|
// Si un DL est actif sur cette même row, on doit d'abord l'annuler proprement.
|
||||||
var st = _downloadManager.GetResumableState(row.Version);
|
var st = _downloadManager.GetResumableState(row.Version);
|
||||||
var sizeStr = FormatSize(st?.DownloadedBytes ?? 0);
|
var sizeStr = FormatSize(st?.DownloadedBytes ?? 0);
|
||||||
var confirm = MessageBox.Show(
|
var confirm = ThemedMessageBox.Show(
|
||||||
Strings.MsgRestartDownloadConfirm(row.Version, sizeStr),
|
Strings.MsgRestartDownloadConfirm(row.Version, sizeStr),
|
||||||
Strings.MsgBoxRestartDl,
|
Strings.MsgBoxRestartDl,
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
|
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
|
||||||
@@ -605,7 +605,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Launch failed");
|
_logger.LogError(ex, "Launch failed");
|
||||||
MessageBox.Show(Strings.MsgLaunchFailed(ex.Message),
|
ThemedMessageBox.Show(Strings.MsgLaunchFailed(ex.Message),
|
||||||
Strings.MsgBoxLaunchError, MessageBoxButton.OK, MessageBoxImage.Error);
|
Strings.MsgBoxLaunchError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -613,7 +613,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
private async Task InstallVersionAsync(VersionRowViewModel row)
|
private async Task InstallVersionAsync(VersionRowViewModel row)
|
||||||
{
|
{
|
||||||
if (row.Remote is null) return;
|
if (row.Remote is null) return;
|
||||||
if (IsBusy) { MessageBox.Show(Strings.MsgBusy, Strings.MsgBoxPatience, MessageBoxButton.OK, MessageBoxImage.Information); return; }
|
if (IsBusy) { ThemedMessageBox.Show(Strings.MsgBusy, Strings.MsgBoxPatience, MessageBoxButton.OK, MessageBoxImage.Information); return; }
|
||||||
|
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
_activeRow = row;
|
_activeRow = row;
|
||||||
@@ -738,7 +738,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
// notifie l'utilisateur que la DB n'a pas été migrée — la prochaine relance
|
// notifie l'utilisateur que la DB n'a pas été migrée — la prochaine relance
|
||||||
// de l'install (ou un Settings → "Rejouer migrations") tentera à nouveau.
|
// de l'install (ou un Settings → "Rejouer migrations") tentera à nouveau.
|
||||||
_logger.LogError("DB migration failed for v{Version}: {Detail}", row.Version, migResult.FailureMessage);
|
_logger.LogError("DB migration failed for v{Version}: {Detail}", row.Version, migResult.FailureMessage);
|
||||||
MessageBox.Show(
|
ThemedMessageBox.Show(
|
||||||
Strings.MsgMigrationFailed(migResult.FailureMessage ?? "?"),
|
Strings.MsgMigrationFailed(migResult.FailureMessage ?? "?"),
|
||||||
Strings.MsgBoxMigrationFailed,
|
Strings.MsgBoxMigrationFailed,
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
@@ -779,13 +779,13 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
_logger.LogInformation("No _report/ in v{Version} ZIP, report tool unchanged", row.Version);
|
_logger.LogInformation("No _report/ in v{Version} ZIP, report tool unchanged", row.Version);
|
||||||
break;
|
break;
|
||||||
case DeployStatus.XamppNotFound:
|
case DeployStatus.XamppNotFound:
|
||||||
MessageBox.Show(
|
ThemedMessageBox.Show(
|
||||||
Strings.MsgReportXamppNotFound(_config.ReportTool.HtdocsRoot),
|
Strings.MsgReportXamppNotFound(_config.ReportTool.HtdocsRoot),
|
||||||
Strings.MsgBoxReportDeployFailed,
|
Strings.MsgBoxReportDeployFailed,
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
break;
|
break;
|
||||||
case DeployStatus.Failed:
|
case DeployStatus.Failed:
|
||||||
MessageBox.Show(
|
ThemedMessageBox.Show(
|
||||||
Strings.MsgReportDeployFailed(rdResult.ErrorMessage ?? "?"),
|
Strings.MsgReportDeployFailed(rdResult.ErrorMessage ?? "?"),
|
||||||
Strings.MsgBoxReportDeployFailed,
|
Strings.MsgBoxReportDeployFailed,
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
@@ -818,7 +818,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
row.State = VersionRowState.AvailableIdle;
|
row.State = VersionRowState.AvailableIdle;
|
||||||
row.ProgressDetail = null;
|
row.ProgressDetail = null;
|
||||||
row.ProgressPercent = 0;
|
row.ProgressPercent = 0;
|
||||||
MessageBox.Show(
|
ThemedMessageBox.Show(
|
||||||
Strings.MsgInstallFailed(ex.Message),
|
Strings.MsgInstallFailed(ex.Message),
|
||||||
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
|
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
StatusMessage = Strings.StatusError(ex.Message);
|
StatusMessage = Strings.StatusError(ex.Message);
|
||||||
@@ -841,14 +841,14 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
if (row.Installed is null) return;
|
if (row.Installed is null) return;
|
||||||
|
|
||||||
var sizeStr = FormatSize(row.SizeBytes);
|
var sizeStr = FormatSize(row.SizeBytes);
|
||||||
var confirm = MessageBox.Show(
|
var confirm = ThemedMessageBox.Show(
|
||||||
Strings.MsgUninstallConfirm(row.Version, row.FolderPath, sizeStr),
|
Strings.MsgUninstallConfirm(row.Version, row.FolderPath, sizeStr),
|
||||||
Strings.MsgBoxConfirm,
|
Strings.MsgBoxConfirm,
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
|
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
|
||||||
|
|
||||||
if (confirm != MessageBoxResult.Yes) return;
|
if (confirm != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
if (IsBusy) { MessageBox.Show(Strings.MsgBusy, Strings.MsgBoxPatience, MessageBoxButton.OK, MessageBoxImage.Information); return; }
|
if (IsBusy) { ThemedMessageBox.Show(Strings.MsgBusy, Strings.MsgBoxPatience, MessageBoxButton.OK, MessageBoxImage.Information); return; }
|
||||||
|
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
row.State = VersionRowState.Uninstalling;
|
row.State = VersionRowState.Uninstalling;
|
||||||
@@ -863,7 +863,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Uninstall failed for {Version}", row.Version);
|
_logger.LogError(ex, "Uninstall failed for {Version}", row.Version);
|
||||||
row.State = VersionRowState.InstalledIdle;
|
row.State = VersionRowState.InstalledIdle;
|
||||||
MessageBox.Show(Strings.MsgUninstallFailed(ex.Message), Strings.MsgBoxError,
|
ThemedMessageBox.Show(Strings.MsgUninstallFailed(ex.Message), Strings.MsgBoxError,
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
StatusMessage = Strings.StatusError(ex.Message);
|
StatusMessage = Strings.StatusError(ex.Message);
|
||||||
}
|
}
|
||||||
@@ -878,7 +878,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
var url = row.Remote?.ReleaseNotesUrl;
|
var url = row.Remote?.ReleaseNotesUrl;
|
||||||
if (string.IsNullOrEmpty(url))
|
if (string.IsNullOrEmpty(url))
|
||||||
{
|
{
|
||||||
MessageBox.Show(Strings.MsgNoReleaseNotes,
|
ThemedMessageBox.Show(Strings.MsgNoReleaseNotes,
|
||||||
Strings.MsgBoxReleaseNotes, MessageBoxButton.OK, MessageBoxImage.Information);
|
Strings.MsgBoxReleaseNotes, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -887,7 +887,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to fetch release notes");
|
_logger.LogWarning(ex, "Failed to fetch release notes");
|
||||||
MessageBox.Show(Strings.MsgReleaseNotesFetchFailed(ex.Message),
|
ThemedMessageBox.Show(Strings.MsgReleaseNotesFetchFailed(ex.Message),
|
||||||
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Warning);
|
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Windows;
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PSLauncher.App.Views;
|
||||||
using PSLauncher.Core.Configuration;
|
using PSLauncher.Core.Configuration;
|
||||||
using PSLauncher.Core.Downloads;
|
using PSLauncher.Core.Downloads;
|
||||||
using PSLauncher.Core.Installations;
|
using PSLauncher.Core.Installations;
|
||||||
@@ -239,7 +240,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ClearCache()
|
private void ClearCache()
|
||||||
{
|
{
|
||||||
var confirm = MessageBox.Show(
|
var confirm = ThemedMessageBox.Show(
|
||||||
Strings.MsgClearCacheConfirm,
|
Strings.MsgClearCacheConfirm,
|
||||||
Strings.MsgBoxConfirm,
|
Strings.MsgBoxConfirm,
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
|
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
|
||||||
@@ -256,7 +257,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "ClearCache failed");
|
_logger.LogError(ex, "ClearCache failed");
|
||||||
MessageBox.Show(ex.Message, Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
|
ThemedMessageBox.Show(ex.Message, Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +305,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
// Le launcher doit être relancé pour que les bindings x:Static reflètent
|
// Le launcher doit être relancé pour que les bindings x:Static reflètent
|
||||||
// la nouvelle langue. Plutôt que de demander à l'utilisateur de fermer/rouvrir,
|
// la nouvelle langue. Plutôt que de demander à l'utilisateur de fermer/rouvrir,
|
||||||
// on spawne un cmd qui attend la fermeture puis relance.
|
// on spawne un cmd qui attend la fermeture puis relance.
|
||||||
var ok = MessageBox.Show(
|
var ok = ThemedMessageBox.Show(
|
||||||
Strings.MsgLanguageRestart,
|
Strings.MsgLanguageRestart,
|
||||||
Strings.MsgBoxLanguageChange,
|
Strings.MsgBoxLanguageChange,
|
||||||
MessageBoxButton.OKCancel, MessageBoxImage.Information, MessageBoxResult.OK);
|
MessageBoxButton.OKCancel, MessageBoxImage.Information, MessageBoxResult.OK);
|
||||||
@@ -335,7 +336,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void DeactivateLicense()
|
private void DeactivateLicense()
|
||||||
{
|
{
|
||||||
var confirm = MessageBox.Show(
|
var confirm = ThemedMessageBox.Show(
|
||||||
Strings.MsgDeactivateLicenseConfirm,
|
Strings.MsgDeactivateLicenseConfirm,
|
||||||
Strings.MsgBoxConfirm,
|
Strings.MsgBoxConfirm,
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||||||
@@ -526,7 +527,7 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
var localDate = backup.TimestampUtc.ToLocalTime();
|
var localDate = backup.TimestampUtc.ToLocalTime();
|
||||||
var dateLabel = Strings.FormatLongDate(localDate);
|
var dateLabel = Strings.FormatLongDate(localDate);
|
||||||
var confirm = MessageBox.Show(
|
var confirm = ThemedMessageBox.Show(
|
||||||
Strings.MsgRevertReportConfirm(dateLabel),
|
Strings.MsgRevertReportConfirm(dateLabel),
|
||||||
Strings.MsgBoxRevertReport,
|
Strings.MsgBoxRevertReport,
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public partial class LicenseDetailsDialog : Window
|
|||||||
|
|
||||||
private void OnDeactivate(object sender, RoutedEventArgs e)
|
private void OnDeactivate(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var confirm = MessageBox.Show(
|
var confirm = ThemedMessageBox.Show(
|
||||||
Strings.MsgDeactivateLicenseDetailedConfirm,
|
Strings.MsgDeactivateLicenseDetailedConfirm,
|
||||||
Strings.MsgBoxConfirm,
|
Strings.MsgBoxConfirm,
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public partial class MainWindow : Window
|
|||||||
if (DataContext is MainViewModel vm && vm.HasActiveDownload)
|
if (DataContext is MainViewModel vm && vm.HasActiveDownload)
|
||||||
{
|
{
|
||||||
var version = vm.ActiveDownloadVersion ?? "?";
|
var version = vm.ActiveDownloadVersion ?? "?";
|
||||||
var result = MessageBox.Show(
|
var result = ThemedMessageBox.Show(
|
||||||
Strings.MsgQuitWhileDownloadingConfirm(version),
|
Strings.MsgQuitWhileDownloadingConfirm(version),
|
||||||
Strings.MsgBoxQuit,
|
Strings.MsgBoxQuit,
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||||||
|
|||||||
72
src/PSLauncher.App/Views/ThemedMessageBox.xaml
Normal file
72
src/PSLauncher.App/Views/ThemedMessageBox.xaml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<Window x:Class="PSLauncher.App.Views.ThemedMessageBox"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:loc="clr-namespace:PSLauncher.Core.Localization;assembly=PSLauncher.Core"
|
||||||
|
Title=""
|
||||||
|
Icon="pack://application:,,,/Resources/favicon.ico"
|
||||||
|
SizeToContent="Height"
|
||||||
|
Width="480"
|
||||||
|
MinHeight="180"
|
||||||
|
MaxHeight="600"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{StaticResource Brush.Bg.Window}">
|
||||||
|
<Border BorderBrush="{StaticResource Brush.Border}" BorderThickness="1">
|
||||||
|
<Grid Margin="24,20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
x:Name="TitleText"
|
||||||
|
FontSize="16" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
Margin="0,0,0,12" />
|
||||||
|
|
||||||
|
<!-- Icon + Message -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
x:Name="IconText"
|
||||||
|
FontSize="36"
|
||||||
|
Margin="0,2,16,0"
|
||||||
|
VerticalAlignment="Top" />
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
x:Name="MessageText"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="13" LineHeight="20"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<StackPanel Grid.Row="2"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,24,0,0">
|
||||||
|
<Button x:Name="Btn1"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
MinWidth="80" Padding="16,8" Margin="0,0,8,0"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Click="OnBtn1Click" />
|
||||||
|
<Button x:Name="Btn2"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
MinWidth="80" Padding="16,8" Margin="0,0,8,0"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Click="OnBtn2Click" />
|
||||||
|
<Button x:Name="Btn3"
|
||||||
|
Style="{StaticResource AccentButton}"
|
||||||
|
MinWidth="80" Padding="16,8"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Click="OnBtn3Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
187
src/PSLauncher.App/Views/ThemedMessageBox.xaml.cs
Normal file
187
src/PSLauncher.App/Views/ThemedMessageBox.xaml.cs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -102,6 +102,9 @@ public static class Strings
|
|||||||
public static string ActionInstall => T("⬇ Installer", "⬇ Install", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت");
|
public static string ActionInstall => T("⬇ Installer", "⬇ Install", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت");
|
||||||
public static string ActionInstallBig => T("⬇ INSTALLER", "⬇ INSTALL", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت");
|
public static string ActionInstallBig => T("⬇ INSTALLER", "⬇ INSTALL", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت");
|
||||||
public static string ActionCancel => T("Annuler", "Cancel", "取消", "ยกเลิก", "إلغاء");
|
public static string ActionCancel => T("Annuler", "Cancel", "取消", "ยกเลิก", "إلغاء");
|
||||||
|
public static string ActionOk => T("OK", "OK", "确定", "ตกลง", "موافق");
|
||||||
|
public static string ActionYes => T("Oui", "Yes", "是", "ใช่", "نعم");
|
||||||
|
public static string ActionNo => T("Non", "No", "否", "ไม่", "لا");
|
||||||
public static string ActionLater => T("Plus tard", "Later", "稍后", "ภายหลัง", "لاحقاً");
|
public static string ActionLater => T("Plus tard", "Later", "稍后", "ภายหลัง", "لاحقاً");
|
||||||
public static string ActionSave => T("Enregistrer", "Save", "保存", "บันทึก", "حفظ");
|
public static string ActionSave => T("Enregistrer", "Save", "保存", "บันทึก", "حفظ");
|
||||||
public static string ActionClose => T("Fermer", "Close", "关闭", "ปิด", "إغلاق");
|
public static string ActionClose => T("Fermer", "Close", "关闭", "ปิด", "إغلاق");
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblyName>PS_Launcher.Updater</AssemblyName>
|
<AssemblyName>PS_Launcher.Updater</AssemblyName>
|
||||||
<RootNamespace>PSLauncher.Updater</RootNamespace>
|
<RootNamespace>PSLauncher.Updater</RootNamespace>
|
||||||
<Version>0.15.0</Version>
|
<Version>0.16.0</Version>
|
||||||
<ApplicationIcon>..\PSLauncher.App\Resources\favicon.ico</ApplicationIcon>
|
<ApplicationIcon>..\PSLauncher.App\Resources\favicon.ico</ApplicationIcon>
|
||||||
<Company>ASTERION VR</Company>
|
<Company>ASTERION VR</Company>
|
||||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||||
|
|||||||
Reference in New Issue
Block a user