Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<Style Selector="Border.settings-card-clickable:pointerover">
<Setter Property="Background" Value="{DynamicResource SettingsCardHoverBackground}"/>
</Style>
<Style Selector="Border.settings-card-focused">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="BorderThickness" Value="2"/>
</Style>

<!-- Setting warning subtext (amber, both themes) -->
<Style Selector="TextBlock.setting-warning-text">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Avalonia.Automation;
using Avalonia.Threading;

namespace UniGetUI.Avalonia.Infrastructure;

public sealed record AccessibilityAnnouncement(
string Message,
AutomationLiveSetting LiveSetting = AutomationLiveSetting.Polite);

public static class AccessibilityAnnouncementService
{
public static event EventHandler<AccessibilityAnnouncement>? AnnouncementRequested;

public static void Announce(
string? message,
AutomationLiveSetting liveSetting = AutomationLiveSetting.Polite)
{
if (string.IsNullOrWhiteSpace(message))
return;

Dispatcher.UIThread.Post(() =>
AnnouncementRequested?.Invoke(
null,
new AccessibilityAnnouncement(message, liveSetting)));
}
}
61 changes: 37 additions & 24 deletions src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
Expand Down Expand Up @@ -113,6 +114,18 @@ private static void ShowOperationProgressNotification(AbstractOperation op)
if (Settings.AreProgressNotificationsDisabled())
return;

string title = op.Metadata.Title.Length > 0
? op.Metadata.Title
: CoreTools.Translate("Operation in progress");

string message = op.Metadata.Status.Length > 0
? op.Metadata.Status
: CoreTools.Translate("Please wait...");

AccessibilityAnnouncementService.Announce(
$"{title}. {message}",
AutomationLiveSetting.Polite);

if (WindowsAppNotificationBridge.ShowProgress(op))
return;

Expand All @@ -122,14 +135,6 @@ private static void ShowOperationProgressNotification(AbstractOperation op)
if (TryGetMainWindow() is not { } mainWindow)
return;

string title = op.Metadata.Title.Length > 0
? op.Metadata.Title
: CoreTools.Translate("Operation in progress");

string message = op.Metadata.Status.Length > 0
? op.Metadata.Status
: CoreTools.Translate("Please wait...");

mainWindow.ShowRuntimeNotification(
title,
message,
Expand All @@ -141,6 +146,18 @@ private static void ShowOperationSuccessNotification(AbstractOperation op)
if (Settings.AreSuccessNotificationsDisabled())
return;

string title = op.Metadata.SuccessTitle.Length > 0
? op.Metadata.SuccessTitle
: CoreTools.Translate("Success!");

string message = op.Metadata.SuccessMessage.Length > 0
? op.Metadata.SuccessMessage
: CoreTools.Translate("Success!");

AccessibilityAnnouncementService.Announce(
$"{title}. {message}",
AutomationLiveSetting.Polite);

WindowsAppNotificationBridge.RemoveProgress(op);

if (WindowsAppNotificationBridge.ShowSuccess(op))
Expand All @@ -152,14 +169,6 @@ private static void ShowOperationSuccessNotification(AbstractOperation op)
if (TryGetMainWindow() is not { } mainWindow)
return;

string title = op.Metadata.SuccessTitle.Length > 0
? op.Metadata.SuccessTitle
: CoreTools.Translate("Success!");

string message = op.Metadata.SuccessMessage.Length > 0
? op.Metadata.SuccessMessage
: CoreTools.Translate("Success!");

mainWindow.ShowRuntimeNotification(
title,
message,
Expand All @@ -171,6 +180,18 @@ private static void ShowOperationFailureNotification(AbstractOperation op)
if (Settings.AreErrorNotificationsDisabled())
return;

string title = op.Metadata.FailureTitle.Length > 0
? op.Metadata.FailureTitle
: CoreTools.Translate("Failed");

string message = op.Metadata.FailureMessage.Length > 0
? op.Metadata.FailureMessage
: CoreTools.Translate("An error occurred while processing this package");

AccessibilityAnnouncementService.Announce(
$"{title}. {message}",
AutomationLiveSetting.Assertive);

WindowsAppNotificationBridge.RemoveProgress(op);

if (WindowsAppNotificationBridge.ShowError(op))
Expand All @@ -182,14 +203,6 @@ private static void ShowOperationFailureNotification(AbstractOperation op)
if (TryGetMainWindow() is not { } mainWindow)
return;

string title = op.Metadata.FailureTitle.Length > 0
? op.Metadata.FailureTitle
: CoreTools.Translate("Failed");

string message = op.Metadata.FailureMessage.Length > 0
? op.Metadata.FailureMessage
: CoreTools.Translate("An error occurred while processing this package");

mainWindow.ShowRuntimeNotification(
title,
message,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public partial class IgnoredPackageEntryViewModel : ObservableObject
public string ManagerIconPath { get; }
public string VersionDisplay { get; }
public string NewVersion { get; }
public string AutomationName { get; }
public string RemoveAutomationName { get; }

private readonly string _ignoredId;

Expand All @@ -154,6 +156,10 @@ public IgnoredPackageEntryViewModel(
ManagerIconPath = managerIconPath;
VersionDisplay = versionDisplay;
NewVersion = newVersion;
AutomationName = CoreTools.Translate("Package {name} from {manager}")
.Replace("{name}", Name)
.Replace("{manager}", Manager);
RemoveAutomationName = CoreTools.Translate("Remove {0} from ignored updates", Name);
}

[RelayCommand]
Expand Down
48 changes: 45 additions & 3 deletions src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
Expand Down Expand Up @@ -50,6 +51,12 @@
public event EventHandler<bool>? CanGoBackChanged;
public event EventHandler<PageType>? CurrentPageChanged;

[ObservableProperty]
private string _announcementText = "";

[ObservableProperty]
private AutomationLiveSetting _announcementLiveSetting = AutomationLiveSetting.Polite;

// ─── Operations panel ─────────────────────────────────────────────────────
public AvaloniaList<OperationViewModel> Operations => AvaloniaOperationRegistry.OperationViewModels;

Expand Down Expand Up @@ -113,6 +120,8 @@

public MainWindowViewModel()
{
AccessibilityAnnouncementService.AnnouncementRequested += OnAnnouncementRequested;

DiscoverPage = new DiscoverSoftwarePage();
UpdatesPage = new SoftwareUpdatesPage();
InstalledPage = new InstalledPackagesPage();
Expand Down Expand Up @@ -203,6 +212,15 @@
LoadDefaultPage();
}

private void OnAnnouncementRequested(object? sender, AccessibilityAnnouncement announcement)

Check warning on line 215 in src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Remove unused parameter 'sender'
{
AnnouncementLiveSetting = announcement.LiveSetting;
AnnouncementText = string.Empty;
Dispatcher.UIThread.Post(
() => AnnouncementText = announcement.Message,
DispatcherPriority.Background);
}

// ─── Navigation ──────────────────────────────────────────────────────────
public void LoadDefaultPage()
{
Expand Down Expand Up @@ -265,9 +283,14 @@
if (newPage_t is PageType.About) { _ = ShowAboutDialog(); return; }
if (newPage_t is PageType.Quit) { (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); return; }

Sidebar.SelectNavButtonForPage(newPage_t);
if (_currentPage == newPage_t)
{
// Re-focus the primary control even when we're already on the page
(CurrentPageContent as AbstractPackagesPage)?.FocusPackageList();
return;
}

if (_currentPage == newPage_t) return;
Sidebar.SelectNavButtonForPage(newPage_t);

var newPage = GetPageForType(newPage_t);
var oldPage = CurrentPageContent as Control;
Expand All @@ -286,7 +309,6 @@
CanGoBackChanged?.Invoke(this, true);
}

(newPage as AbstractPackagesPage)?.FocusPackageList();
(newPage as AbstractPackagesPage)?.FilterPackages();
(newPage as IEnterLeaveListener)?.OnEnter();

Expand All @@ -305,9 +327,29 @@
GlobalSearchEnabled = false;
}

// Focus after search state is restored so MegaQueryVisible is already correct
(newPage as AbstractPackagesPage)?.FocusPackageList();

AccessibilityAnnouncementService.Announce(GetPageAnnouncement(newPage_t));
CurrentPageChanged?.Invoke(this, newPage_t);
}

private static string GetPageAnnouncement(PageType pageType) => pageType switch
{
PageType.Discover => CoreTools.Translate("Discover Packages"),
PageType.Updates => CoreTools.Translate("Software Updates"),
PageType.Installed => CoreTools.Translate("Installed Packages"),
PageType.Bundles => CoreTools.Translate("Package Bundles"),
PageType.Settings => CoreTools.Translate("Settings"),
PageType.Managers => CoreTools.Translate("Package Managers"),
PageType.OwnLog => CoreTools.Translate("UniGetUI Log"),
PageType.ManagerLog => CoreTools.Translate("Package Manager logs"),
PageType.OperationHistory => CoreTools.Translate("Operation history"),
PageType.Help => CoreTools.Translate("Help"),
PageType.ReleaseNotes => CoreTools.Translate("Release notes"),
_ => CoreTools.Translate("UniGetUI"),
};

public void NavigateBack()
{
if (CurrentPageContent is IInnerNavigationPage navPage && navPage.CanGoBack())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.IO;
using Avalonia.Automation;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Platform.Storage;
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Avalonia.ViewModels;
using UniGetUI.Avalonia.Views.DialogPages;
using UniGetUI.Avalonia.Views.Pages.SettingsPages;
Expand Down Expand Up @@ -37,6 +40,9 @@ private async Task ImportSettings(Visual? visual)
var path = file.TryGetLocalPath();
if (path is null) return;
await Task.Run(() => CoreSettings.ImportFromFile_JSON(path));
AccessibilityAnnouncementService.Announce(
CoreTools.Translate("Settings imported from {0}", Path.GetFileName(path)),
AutomationLiveSetting.Polite);
OnRestartRequired();
}

Expand All @@ -52,7 +58,13 @@ private static async Task ExportSettings(Visual? visual)
if (file is null) return;
var path = file.TryGetLocalPath();
if (path is null) return;
try { await Task.Run(() => CoreSettings.ExportToFile_JSON(path)); }
try
{
await Task.Run(() => CoreSettings.ExportToFile_JSON(path));
AccessibilityAnnouncementService.Announce(
CoreTools.Translate("Settings exported to {0}", Path.GetFileName(path)),
AutomationLiveSetting.Polite);
}
catch (Exception ex) { Logger.Error(ex); }
}

Expand All @@ -61,6 +73,9 @@ private void ResetSettings(Visual? _)
{
try { CoreSettings.ResetSettings(); }
catch (Exception ex) { Logger.Error(ex); }
AccessibilityAnnouncementService.Announce(
CoreTools.Translate("UniGetUI settings were reset"),
AutomationLiveSetting.Assertive);
OnRestartRequired();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.ComponentModel;
using System.Globalization;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Layout;
Expand Down Expand Up @@ -260,6 +261,7 @@ public Button AddToolbarButton(string svgName, string label, Action onClick, boo
Content = content,
};
ToolTip.SetTip(btn, label);
AutomationProperties.SetName(btn, label);
btn.Click += (_, _) => onClick();
ToolBarItems.Add(btn);
return btn;
Expand All @@ -268,14 +270,16 @@ public Button AddToolbarButton(string svgName, string label, Action onClick, boo
/// <summary>Adds a thin vertical separator to the toolbar.</summary>
public void AddToolbarSeparator()
{
ToolBarItems.Add(new Separator
var sep = new Separator
{
Width = 1,
Height = 30,
Margin = new Thickness(4, 4),
Background = Application.Current?.FindResource("AppBorderBrush") as IBrush
?? new SolidColorBrush(Color.FromArgb(60, 255, 255, 255)),
});
};
AutomationProperties.SetAccessibilityView(sep, AccessibilityView.Raw);
ToolBarItems.Add(sep);
}

public async Task ShowInfoDialog(Window owner, string title, string message)
Expand Down Expand Up @@ -759,11 +763,15 @@ public void ToggleSelectAll()
{
AllPackagesChecked = true;
FilteredPackages.SelectAll();
AccessibilityAnnouncementService.Announce(
CoreTools.Translate("All packages selected"));
}
else
{
AllPackagesChecked = false;
FilteredPackages.ClearSelection();
AccessibilityAnnouncementService.Announce(
CoreTools.Translate("Package selection cleared"));
}
}

Expand Down
Loading
Loading