-
Notifications
You must be signed in to change notification settings - Fork 793
Expand file tree
/
Copy pathAbstractPackagesPage.axaml.cs
More file actions
234 lines (199 loc) · 10.1 KB
/
AbstractPackagesPage.axaml.cs
File metadata and controls
234 lines (199 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Threading;
using UniGetUI.Avalonia.ViewModels.Pages;
using UniGetUI.Avalonia.Views.Controls;
using UniGetUI.Core.Tools;
using UniGetUI.PackageEngine.Interfaces;
using UniGetUI.PackageEngine.PackageClasses;
namespace UniGetUI.Avalonia.Views.Pages;
public abstract partial class AbstractPackagesPage : UserControl,
IKeyboardShortcutListener, IEnterLeaveListener, ISearchBoxPage
{
public PackagesPageViewModel ViewModel => (PackagesPageViewModel)DataContext!;
protected AbstractPackagesPage(PackagesPageData data)
{
// InitializeComponent BEFORE setting DataContext so that the svg:Svg
// Path binding has no context during XamlIlPopulate — Skia crashes if
// it tries to load an SVG synchronously mid-init on macOS.
InitializeComponent();
DataContext = new PackagesPageViewModel(data);
// Wire ViewModel events that need UI access
ViewModel.FocusListRequested += OnFocusListRequested;
ViewModel.HelpRequested += () => GetMainWindow()?.Navigate(PageType.Help);
ViewModel.ManageIgnoredRequested += async () =>
{
if (GetMainWindow() is { } win)
await new ManageIgnoredUpdatesWindow().ShowDialog(win);
};
ViewModel.SharePackageRequested += async (pkgName, url) =>
{
if (GetMainWindow() is not { } win) return;
if (url is null)
{
await ViewModel.ShowInfoDialog(win,
CoreTools.Translate("Nothing to share"),
CoreTools.Translate("Please select a package first."));
return;
}
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard is not null) await clipboard.SetTextAsync(url);
await ViewModel.ShowInfoDialog(win,
CoreTools.Translate("Share link copied"),
CoreTools.Translate("The share link for {0} has been copied to the clipboard.", pkgName ?? ""));
};
// "New version" sort option is only relevant on the updates page
OrderByNewVersion_Menu.IsVisible = ViewModel.RoleIsUpdateLike;
// Stamp initial checkmarks, then keep them in sync with sort-property changes
UpdateSortMenuChecks();
ViewModel.PropertyChanged += (_, args) =>
{
if (args.PropertyName is nameof(PackagesPageViewModel.SortFieldIndex)
or nameof(PackagesPageViewModel.SortAscending))
UpdateSortMenuChecks();
};
// Build the toolbar now that both AXAML controls and the ViewModel are ready
GenerateToolBar(ViewModel);
// Double-click a list row → show details
PackageList.DoubleTapped += (_, _) => _ = ShowDetailsForPackage(SelectedItem);
// Keyboard shortcuts on the package list
PackageList.KeyDown += PackageList_KeyDown;
// Wire context menu (built by subclass)
var contextMenu = GenerateContextMenu();
if (contextMenu is not null)
{
PackageList.ContextMenu = contextMenu;
contextMenu.Opening += (_, _) =>
{
var pkg = SelectedItem;
if (pkg is not null) WhenShowingContextMenu(pkg);
};
}
}
// ─── UI-only: focus the package list ─────────────────────────────────────
private void OnFocusListRequested() => PackageList.Focus();
public void FocusPackageList()
{
if (ViewModel.MegaQueryBoxEnabled)
Dispatcher.UIThread.Post(() => MegaQueryBlock.Focus(), DispatcherPriority.Background);
else
ViewModel.RequestFocusList();
}
public void FilterPackages() => ViewModel.FilterPackages();
// ─── Abstract: let concrete pages add toolbar items ───────────────────────
protected abstract void GenerateToolBar(PackagesPageViewModel vm);
// ─── Abstract: per-page actions invoked by base class keyboard/mouse handlers ─
/// <summary>Performs the page's primary action (install / uninstall / update) on the package.</summary>
protected abstract void PerformMainPackageAction(IPackage? package);
/// <summary>Opens the details dialog for the package.</summary>
protected abstract Task ShowDetailsForPackage(IPackage? package);
/// <summary>Opens the installation-options dialog for the package.</summary>
protected abstract Task ShowInstallationOptionsForPackage(IPackage? package);
// ─── Virtual: let concrete pages supply a context menu ────────────────────
protected virtual ContextMenu? GenerateContextMenu() => null;
protected virtual void WhenShowingContextMenu(IPackage package) { }
// ─── Helper: create a 16×16 SvgIcon for use as a menu item icon ───────────
protected static SvgIcon LoadMenuIcon(string svgName) => new()
{
Path = $"avares://UniGetUI.Avalonia/Assets/Symbols/{svgName}.svg",
Width = 16,
Height = 16,
};
// ─── Protected access to main toolbar controls for subclasses ─────────────
/// <summary>Sets the icon and text of the primary action button.</summary>
protected void SetMainButton(string svgName, string label, Action onClick)
{
MainToolbarButtonIcon.Path = $"avares://UniGetUI.Avalonia/Assets/Symbols/{svgName}.svg";
MainToolbarButtonText.Text = label;
MainToolbarButton.Click += (_, _) => onClick();
}
/// <summary>Sets the dropdown flyout of the primary action button.</summary>
protected void SetMainButtonDropdown(MenuFlyout flyout)
{
MainToolbarButtonDropdown.Flyout = flyout;
}
// ─── Package selection ────────────────────────────────────────────────────
/// <summary>
/// Returns the focused row's package, or the single checked package if
/// nothing is focused. Mirrors the WinUI SelectedItem pattern.
/// </summary>
protected IPackage? SelectedItem
{
get
{
if (PackageList.SelectedItem is PackageWrapper w)
return w.Package;
var checked_ = ViewModel.FilteredPackages.GetCheckedPackages();
if (checked_.Count == 1)
return checked_.First();
return null;
}
}
// ─── Operation launchers (delegated to ViewModel) ─────────────────────────
protected static Task LaunchInstall(
IEnumerable<IPackage> packages,
bool? elevated = null,
bool? interactive = null,
bool? no_integrity = null)
=> PackagesPageViewModel.LaunchInstall(packages, elevated, interactive, no_integrity);
// ─── Sort menu checkmarks (UI reacts to ViewModel sort changes) ───────────
private static TextBlock? Check(bool show) =>
show ? new TextBlock { Text = "✓", FontSize = 12 } : null;
private void UpdateSortMenuChecks()
{
OrderByName_Menu.Icon = Check(ViewModel.SortFieldIndex == 0);
OrderById_Menu.Icon = Check(ViewModel.SortFieldIndex == 1);
OrderByVersion_Menu.Icon = Check(ViewModel.SortFieldIndex == 2);
OrderByNewVersion_Menu.Icon = Check(ViewModel.SortFieldIndex == 3);
OrderBySource_Menu.Icon = Check(ViewModel.SortFieldIndex == 4);
OrderByAscending_Menu.Icon = Check(ViewModel.SortAscending);
OrderByDescending_Menu.Icon = Check(!ViewModel.SortAscending);
}
// ─── IKeyboardShortcutListener ────────────────────────────────────────────
public void SearchTriggered()
{
// TODO: focus global search box
}
public void ReloadTriggered() => ViewModel.TriggerReload();
public void SelectAllTriggered() => ViewModel.ToggleSelectAll();
// ─── IEnterLeaveListener ──────────────────────────────────────────────────
public virtual void OnEnter() { }
public virtual void OnLeave() { }
// ─── ISearchBoxPage ───────────────────────────────────────────────────────
public string QueryBackup
{
get => ViewModel.QueryBackup;
set => ViewModel.QueryBackup = value;
}
public string SearchBoxPlaceholder => ViewModel.SearchBoxPlaceholder;
public void SearchBox_QuerySubmitted(object? sender, EventArgs? e) => ViewModel.HandleSearchSubmitted();
private void MegaQueryBlock_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter || e.Key == Key.Return)
ViewModel.SubmitSearch();
}
private void PackageList_KeyDown(object? sender, KeyEventArgs e)
{
var pkg = SelectedItem;
if (pkg is null) return;
bool ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
bool alt = e.KeyModifiers.HasFlag(KeyModifiers.Alt);
if (e.Key is Key.Enter or Key.Return)
{
if (alt)
_ = ShowInstallationOptionsForPackage(pkg);
else if (ctrl)
PerformMainPackageAction(pkg);
else
_ = ShowDetailsForPackage(pkg);
e.Handled = true;
}
}
// ─── Shared cross-page helpers ────────────────────────────────────────────
protected static MainWindow? GetMainWindow()
=> Application.Current?.ApplicationLifetime
is IClassicDesktopStyleApplicationLifetime { MainWindow: MainWindow w } ? w : null;
}