mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-07 02:49:14 +02:00
Compare commits
4 Commits
niels9001/
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a32c75928b | ||
|
|
c7e53c6ad9 | ||
|
|
52df5882a3 | ||
|
|
0790674ddf |
@@ -7,9 +7,11 @@ using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class DetailsViewModel(IDetails _details, WeakReference<IPageContext> context) : ExtensionObjectViewModel(context)
|
||||
public partial class DetailsViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
private readonly ExtensionObject<IDetails> _detailsModel = new(_details);
|
||||
private readonly ExtensionObject<IDetails> _detailsModel;
|
||||
private INotifyPropChanged? _observableDetails;
|
||||
private bool _isSubscribed;
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
@@ -25,6 +27,81 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
// where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator}
|
||||
public List<DetailsElementViewModel> Metadata { get; private set; } = [];
|
||||
|
||||
public DetailsViewModel(IDetails details, WeakReference<IPageContext> context)
|
||||
: base(context)
|
||||
{
|
||||
_detailsModel = new(details);
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
FetchProperty(args.PropertyName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = _detailsModel.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(IDetails.Title):
|
||||
Title = model.Title ?? string.Empty;
|
||||
UpdateProperty(nameof(Title));
|
||||
break;
|
||||
case nameof(IDetails.Body):
|
||||
Body = model.Body ?? string.Empty;
|
||||
UpdateProperty(nameof(Body));
|
||||
break;
|
||||
case nameof(IDetails.HeroImage):
|
||||
HeroImage = new(model.HeroImage);
|
||||
HeroImage.InitializeProperties();
|
||||
UpdateProperty(nameof(HeroImage));
|
||||
break;
|
||||
case nameof(IDetails.Metadata):
|
||||
RebuildMetadata(model);
|
||||
UpdateProperty(nameof(Metadata));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildMetadata(IDetails model)
|
||||
{
|
||||
var newMetadata = new List<DetailsElementViewModel>();
|
||||
var meta = model.Metadata;
|
||||
if (meta is not null)
|
||||
{
|
||||
foreach (var element in meta)
|
||||
{
|
||||
DetailsElementViewModel? vm = element.Data switch
|
||||
{
|
||||
IDetailsSeparator => new DetailsSeparatorViewModel(element, this.PageContext),
|
||||
IDetailsLink => new DetailsLinkViewModel(element, this.PageContext),
|
||||
IDetailsCommands => new DetailsCommandsViewModel(element, this.PageContext),
|
||||
IDetailsTags => new DetailsTagsViewModel(element, this.PageContext),
|
||||
_ => null,
|
||||
};
|
||||
if (vm is not null)
|
||||
{
|
||||
vm.InitializeProperties();
|
||||
newMetadata.Add(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Metadata = newMetadata;
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _detailsModel.Unsafe;
|
||||
@@ -33,6 +110,14 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to PropChanged if the model supports it (only subscribe once)
|
||||
if (!_isSubscribed && model is INotifyPropChanged observable)
|
||||
{
|
||||
observable.PropChanged += Model_PropChanged;
|
||||
_observableDetails = observable;
|
||||
_isSubscribed = true;
|
||||
}
|
||||
|
||||
Title = model.Title ?? string.Empty;
|
||||
Body = model.Body ?? string.Empty;
|
||||
HeroImage = new(model.HeroImage);
|
||||
@@ -57,25 +142,18 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
|
||||
UpdateProperty(nameof(Size));
|
||||
|
||||
var meta = model.Metadata;
|
||||
if (meta is not null)
|
||||
RebuildMetadata(model);
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
if (_isSubscribed && _observableDetails is not null)
|
||||
{
|
||||
foreach (var element in meta)
|
||||
{
|
||||
DetailsElementViewModel? vm = element.Data switch
|
||||
{
|
||||
IDetailsSeparator => new DetailsSeparatorViewModel(element, this.PageContext),
|
||||
IDetailsLink => new DetailsLinkViewModel(element, this.PageContext),
|
||||
IDetailsCommands => new DetailsCommandsViewModel(element, this.PageContext),
|
||||
IDetailsTags => new DetailsTagsViewModel(element, this.PageContext),
|
||||
_ => null,
|
||||
};
|
||||
if (vm is not null)
|
||||
{
|
||||
vm.InitializeProperties();
|
||||
Metadata.Add(vm);
|
||||
}
|
||||
}
|
||||
_observableDetails.PropChanged -= Model_PropChanged;
|
||||
_observableDetails = null;
|
||||
_isSubscribed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,11 @@ public sealed partial class DockViewModel : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settings == settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings = settings;
|
||||
SetupBands();
|
||||
}
|
||||
|
||||
@@ -158,11 +158,13 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
UpdateProperty(nameof(Type), nameof(IsInteractive));
|
||||
break;
|
||||
case nameof(Details):
|
||||
var existingReference = Details;
|
||||
var extensionDetails = model.Details;
|
||||
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||
Details?.InitializeProperties();
|
||||
UpdateProperty(nameof(Details), nameof(HasDetails));
|
||||
UpdateShowDetailsCommand();
|
||||
existingReference?.SafeCleanup();
|
||||
break;
|
||||
case nameof(model.MoreCommands):
|
||||
AddShowDetailsCommands();
|
||||
|
||||
@@ -48,7 +48,10 @@ public record DockSettings
|
||||
public string? BackgroundImagePath { get; init; }
|
||||
|
||||
// </Theme settings>
|
||||
private ImmutableList<DockBandSettings>? _startBands = ImmutableList.Create(
|
||||
|
||||
// Band lists use EquatableList backing fields so the compiler-synthesized record equality
|
||||
// compares them by content, not by reference. See EquatableList<T> for why this matters.
|
||||
private readonly EquatableList<DockBandSettings> _startBands = new(ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "com.microsoft.cmdpal.builtin.core",
|
||||
@@ -59,23 +62,23 @@ public record DockSettings
|
||||
ProviderId = "WinGet",
|
||||
CommandId = "com.microsoft.cmdpal.winget",
|
||||
ShowTitles = false,
|
||||
});
|
||||
}));
|
||||
|
||||
public ImmutableList<DockBandSettings> StartBands
|
||||
{
|
||||
get => _startBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _startBands = value;
|
||||
get => _startBands.List;
|
||||
init => _startBands = new(value);
|
||||
}
|
||||
|
||||
private ImmutableList<DockBandSettings>? _centerBands = ImmutableList<DockBandSettings>.Empty;
|
||||
private readonly EquatableList<DockBandSettings> _centerBands = new(ImmutableList<DockBandSettings>.Empty);
|
||||
|
||||
public ImmutableList<DockBandSettings> CenterBands
|
||||
{
|
||||
get => _centerBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _centerBands = value;
|
||||
get => _centerBands.List;
|
||||
init => _centerBands = new(value);
|
||||
}
|
||||
|
||||
private ImmutableList<DockBandSettings>? _endBands = ImmutableList.Create(
|
||||
private readonly EquatableList<DockBandSettings> _endBands = new(ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "PerformanceMonitor",
|
||||
@@ -85,12 +88,12 @@ public record DockSettings
|
||||
{
|
||||
ProviderId = "com.microsoft.cmdpal.builtin.datetime",
|
||||
CommandId = "com.microsoft.cmdpal.timedate.dockBand",
|
||||
});
|
||||
}));
|
||||
|
||||
public ImmutableList<DockBandSettings> EndBands
|
||||
{
|
||||
get => _endBands ?? ImmutableList<DockBandSettings>.Empty;
|
||||
init => _endBands = value;
|
||||
get => _endBands.List;
|
||||
init => _endBands = new(value);
|
||||
}
|
||||
|
||||
public bool ShowLabels { get; init; } = true;
|
||||
@@ -99,12 +102,12 @@ public record DockSettings
|
||||
/// Gets the per-monitor dock configurations. Each entry overrides global
|
||||
/// settings for a specific display. Empty by default (all monitors use global).
|
||||
/// </summary>
|
||||
private ImmutableList<DockMonitorConfig>? _monitorConfigs = ImmutableList<DockMonitorConfig>.Empty;
|
||||
private readonly EquatableList<DockMonitorConfig> _monitorConfigs = new(ImmutableList<DockMonitorConfig>.Empty);
|
||||
|
||||
public ImmutableList<DockMonitorConfig> MonitorConfigs
|
||||
{
|
||||
get => _monitorConfigs ?? ImmutableList<DockMonitorConfig>.Empty;
|
||||
init => _monitorConfigs = value ?? ImmutableList<DockMonitorConfig>.Empty;
|
||||
get => _monitorConfigs.List;
|
||||
init => _monitorConfigs = new(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -192,20 +195,41 @@ public sealed record DockMonitorConfig
|
||||
/// </summary>
|
||||
public bool IsCustomized { get; init; }
|
||||
|
||||
// Nullable EquatableList backing fields give the synthesized record equality structural
|
||||
// comparison of the per-monitor bands while preserving null ("inherit global") vs. an
|
||||
// explicit (possibly empty) list. See EquatableList<T>.
|
||||
private readonly EquatableList<DockBandSettings>? _startBands;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-monitor start bands. Only used when <see cref="IsCustomized"/> is <c>true</c>.
|
||||
/// </summary>
|
||||
public ImmutableList<DockBandSettings>? StartBands { get; init; }
|
||||
public ImmutableList<DockBandSettings>? StartBands
|
||||
{
|
||||
get => _startBands?.List;
|
||||
init => _startBands = value is null ? null : new EquatableList<DockBandSettings>(value);
|
||||
}
|
||||
|
||||
private readonly EquatableList<DockBandSettings>? _centerBands;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-monitor center bands. Only used when <see cref="IsCustomized"/> is <c>true</c>.
|
||||
/// </summary>
|
||||
public ImmutableList<DockBandSettings>? CenterBands { get; init; }
|
||||
public ImmutableList<DockBandSettings>? CenterBands
|
||||
{
|
||||
get => _centerBands?.List;
|
||||
init => _centerBands = value is null ? null : new EquatableList<DockBandSettings>(value);
|
||||
}
|
||||
|
||||
private readonly EquatableList<DockBandSettings>? _endBands;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-monitor end bands. Only used when <see cref="IsCustomized"/> is <c>true</c>.
|
||||
/// </summary>
|
||||
public ImmutableList<DockBandSettings>? EndBands { get; init; }
|
||||
public ImmutableList<DockBandSettings>? EndBands
|
||||
{
|
||||
get => _endBands?.List;
|
||||
init => _endBands = value is null ? null : new EquatableList<DockBandSettings>(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC timestamp when this monitor was last seen connected. Used for
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// A thin wrapper around <see cref="ImmutableList{T}"/> that provides <em>structural</em>
|
||||
/// (element-by-element) equality. <see cref="ImmutableList{T}"/> itself only implements
|
||||
/// reference equality, which means a record holding one compares unequal to an otherwise
|
||||
/// identical record whenever the list was rebuilt into a fresh instance (e.g. after loading
|
||||
/// settings from disk).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used as the <em>backing field</em> type for record list properties (the public properties
|
||||
/// still expose <see cref="ImmutableList{T}"/>). Because the compiler-synthesized record
|
||||
/// equality compares backing fields, swapping the field type here makes that synthesized
|
||||
/// equality structural — with no hand-written <c>Equals</c> to keep in sync as new properties
|
||||
/// are added.
|
||||
/// </remarks>
|
||||
internal readonly struct EquatableList<T> : IEquatable<EquatableList<T>>
|
||||
{
|
||||
private readonly ImmutableList<T>? _list;
|
||||
|
||||
public EquatableList(ImmutableList<T>? list) => _list = list;
|
||||
|
||||
public ImmutableList<T> List => _list ?? ImmutableList<T>.Empty;
|
||||
|
||||
public bool Equals(EquatableList<T> other)
|
||||
{
|
||||
var a = List;
|
||||
var b = other.List;
|
||||
|
||||
if (ReferenceEquals(a, b))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.Count != b.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var comparer = EqualityComparer<T>.Default;
|
||||
for (var i = 0; i < a.Count; i++)
|
||||
{
|
||||
if (!comparer.Equals(a[i], b[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is EquatableList<T> other && Equals(other);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = default(HashCode);
|
||||
foreach (var item in List)
|
||||
{
|
||||
hash.Add(item);
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public record SettingsModel
|
||||
{
|
||||
// LOAD BEARNING: Some SettingsChanged subscribers react selectively (e.g.
|
||||
// MainWindow.MainWindowSettingsComparer, DockWindowManager.OnSettingsChanged). If a new
|
||||
// setting needs a live reaction, add it there too - otherwise it won't take effect.
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// SETTINGS HERE
|
||||
public static HotkeySettings DefaultActivationShortcut { get; } = new HotkeySettings(true, false, true, false, 0x20); // win+alt+space
|
||||
|
||||
@@ -55,6 +55,18 @@ public partial class ShellViewModel : ObservableObject,
|
||||
oldValue.PropertyChanged -= CurrentPage_PropertyChanged;
|
||||
value.PropertyChanged += CurrentPage_PropertyChanged;
|
||||
|
||||
// Re-evaluate search-box visibility for the page we're switching to.
|
||||
// CurrentPage_PropertyChanged only reacts to a *change* of HasSearchBox, so
|
||||
// switching to a page whose HasSearchBox already holds its final value (e.g.
|
||||
// navigating back to a list from a ContentPage) would otherwise never restore
|
||||
// the search box. Only force it visible here; hiding it on content pages is
|
||||
// deliberately deferred (see ShellPage.FocusAfterLoaded) so focus doesn't jump
|
||||
// around for screen readers.
|
||||
if (value.HasSearchBox)
|
||||
{
|
||||
IsSearchBoxVisible = true;
|
||||
}
|
||||
|
||||
if (oldValue is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -498,6 +498,12 @@ public sealed partial class SearchBar : UserControl,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves focus to the inner search text box using <see cref="FocusState.Keyboard"/>, which
|
||||
/// (unlike a programmatic <c>Focus</c> on the control) lets the screen reader announce the
|
||||
/// box and its placeholder. Used for summon and post-navigation focus alike so both paths
|
||||
/// are announced consistently.
|
||||
/// </summary>
|
||||
internal void FocusActiveControl()
|
||||
{
|
||||
this.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||
|
||||
@@ -219,6 +219,11 @@ public sealed partial class DockWindow : WindowEx,
|
||||
return;
|
||||
}
|
||||
|
||||
if (_settings == args.DockSettings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings = args.DockSettings;
|
||||
RefreshSideOverride();
|
||||
DispatcherQueue.TryEnqueue(UpdateSettingsOnUiThread);
|
||||
|
||||
@@ -26,6 +26,9 @@ public sealed partial class DockWindowManager : IDisposable
|
||||
private bool _disposed;
|
||||
private int _syncing;
|
||||
|
||||
private bool? _lastSyncedEnableDock;
|
||||
private DockSettings? _lastSyncedDockSettings;
|
||||
|
||||
public DockWindowManager(
|
||||
IMonitorService monitorService,
|
||||
ISettingsService settingsService,
|
||||
@@ -217,6 +220,14 @@ public sealed partial class DockWindowManager : IDisposable
|
||||
|
||||
private void OnSettingsChanged(ISettingsService sender, SettingsModel args)
|
||||
{
|
||||
if (args.EnableDock == _lastSyncedEnableDock && args.DockSettings == _lastSyncedDockSettings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSyncedEnableDock = args.EnableDock;
|
||||
_lastSyncedDockSettings = args.DockSettings;
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (!_disposed)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using CmdPalKeyboardService;
|
||||
@@ -81,6 +82,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private bool _suppressDpiChange;
|
||||
private bool _themeServiceInitialized;
|
||||
|
||||
// The snapshot of settings last consumed by HotReloadSettings. Used to skip redundant
|
||||
// hot-reloads when a SettingsChanged notification touches settings this window doesn't
|
||||
// care about (theme, provider config, aliases, and so on).
|
||||
private SettingsModel? _lastAppliedSettings;
|
||||
|
||||
// Session tracking for telemetry
|
||||
private Stopwatch? _sessionStopwatch;
|
||||
private int _sessionCommandsExecuted;
|
||||
@@ -239,6 +245,14 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void SettingsChangedHandler(ISettingsService sender, SettingsModel args)
|
||||
{
|
||||
// Only rebuild window state when a setting HotReloadSettings actually consumes has
|
||||
// changed. Most SettingsChanged notifications (theme, provider config, aliases, ...)
|
||||
// don't affect the host window, so hot-reloading on every one is wasteful.
|
||||
if (MainWindowSettingsComparer.Instance.Equals(_lastAppliedSettings, args))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DispatcherQueue.TryEnqueue(HotReloadSettings);
|
||||
}
|
||||
|
||||
@@ -464,7 +478,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void HotReloadSettings()
|
||||
{
|
||||
// NOTE: SettingsChangedHandler skips this method when nothing relevant changed, using
|
||||
// MainWindowSettingsComparer. When you start consuming a new setting below, add it to
|
||||
// that comparer too — otherwise changes to it won't trigger a hot-reload.
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
_lastAppliedSettings = settings;
|
||||
|
||||
SetupHotkey(settings);
|
||||
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
|
||||
@@ -489,6 +507,87 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private static bool ShouldShowHwndFrame(SettingsModel settings) =>
|
||||
!BuildInfo.IsCiBuild && settings.ShowHwndFrame;
|
||||
|
||||
/// <summary>
|
||||
/// Compares two <see cref="SettingsModel"/> instances by only the settings that
|
||||
/// <see cref="HotReloadSettings"/> (and the methods it calls) actually consume. Any change
|
||||
/// to a setting outside this set is invisible to the host window, so treating such models
|
||||
/// as equal lets <see cref="SettingsChangedHandler"/> skip a redundant hot-reload.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Keep this in sync with the settings read by <see cref="HotReloadSettings"/>,
|
||||
/// <see cref="SetupHotkey"/>, <see cref="ShouldShowHwndFrame"/>, and
|
||||
/// <see cref="HandleExpandCompactOnUiThread"/>.
|
||||
/// </remarks>
|
||||
private sealed class MainWindowSettingsComparer : IEqualityComparer<SettingsModel>
|
||||
{
|
||||
public static MainWindowSettingsComparer Instance { get; } = new();
|
||||
|
||||
public bool Equals(SettingsModel? x, SettingsModel? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x is null || y is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return x.UseLowLevelGlobalHotkey == y.UseLowLevelGlobalHotkey
|
||||
&& x.ShowSystemTrayIcon == y.ShowSystemTrayIcon
|
||||
&& x.IgnoreShortcutWhenFullscreen == y.IgnoreShortcutWhenFullscreen
|
||||
&& x.IgnoreShortcutWhenBusy == y.IgnoreShortcutWhenBusy
|
||||
&& x.AllowBreakthroughShortcut == y.AllowBreakthroughShortcut
|
||||
&& x.AutoGoHomeInterval == y.AutoGoHomeInterval
|
||||
&& x.ShowHwndFrame == y.ShowHwndFrame
|
||||
&& x.CompactMode == y.CompactMode
|
||||
&& x.Hotkey == y.Hotkey // HotkeySettings is a record (value equality)
|
||||
&& CommandHotkeysEqual(x.CommandHotkeys, y.CommandHotkeys);
|
||||
}
|
||||
|
||||
// TopLevelHotkey is a record (value equality); compare element-wise. ImmutableList
|
||||
// itself only implements reference equality, so we can't rely on ==.
|
||||
private static bool CommandHotkeysEqual(ImmutableList<TopLevelHotkey> x, ImmutableList<TopLevelHotkey> y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x.Count != y.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < x.Count; i++)
|
||||
{
|
||||
if (x[i] != y[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetHashCode(SettingsModel obj)
|
||||
{
|
||||
var hash = default(HashCode);
|
||||
hash.Add(obj.UseLowLevelGlobalHotkey);
|
||||
hash.Add(obj.ShowSystemTrayIcon);
|
||||
hash.Add(obj.IgnoreShortcutWhenFullscreen);
|
||||
hash.Add(obj.IgnoreShortcutWhenBusy);
|
||||
hash.Add(obj.AllowBreakthroughShortcut);
|
||||
hash.Add(obj.AutoGoHomeInterval);
|
||||
hash.Add(obj.ShowHwndFrame);
|
||||
hash.Add(obj.CompactMode);
|
||||
hash.Add(obj.Hotkey);
|
||||
hash.Add(obj.CommandHotkeys.Count);
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the HWND for the borderless / transparent main-window mode and (when
|
||||
/// the internal debug toggle is enabled) overlays the OS-drawn chrome so the HWND's
|
||||
|
||||
@@ -584,9 +584,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
ViewModel.GoHome(withAnimation, focusSearch);
|
||||
}
|
||||
|
||||
if (focusSearch)
|
||||
// Only move focus when the palette is actually on screen. FocusActiveControl uses keyboard
|
||||
// focus so the screen reader announces the box; doing that while the window is hidden
|
||||
// would announce it prematurely.
|
||||
if (focusSearch && HostWindow?.IsVisibleToUser == true)
|
||||
{
|
||||
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
SearchBox.FocusActiveControl();
|
||||
SearchBox.SelectSearch();
|
||||
}
|
||||
}
|
||||
@@ -601,10 +604,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
GoBack(withAnimation, focusSearch: false);
|
||||
}
|
||||
|
||||
// focus search box, even if we were already home
|
||||
if (focusSearch)
|
||||
// focus search box, even if we were already home (but only when the palette is on
|
||||
// screen - see GoBack; keyboard focus while hidden announces prematurely).
|
||||
if (focusSearch && HostWindow?.IsVisibleToUser == true)
|
||||
{
|
||||
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
SearchBox.FocusActiveControl();
|
||||
SearchBox.SelectSearch();
|
||||
}
|
||||
}
|
||||
@@ -813,7 +817,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
return;
|
||||
}
|
||||
|
||||
SearchBox.Focus(FocusState.Programmatic);
|
||||
SearchBox.FocusActiveControl();
|
||||
SearchBox.SelectSearch();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public partial class DetailsViewModelTests
|
||||
{
|
||||
private sealed class TestPageContext : IPageContext
|
||||
{
|
||||
public TaskScheduler Scheduler => TaskScheduler.Default;
|
||||
|
||||
public ICommandProviderContext ProviderContext => CommandProviderContext.Empty;
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint = null)
|
||||
{
|
||||
throw new AssertFailedException($"Unexpected exception from view model: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private static WeakReference<IPageContext> CreatePageContext()
|
||||
{
|
||||
var ctx = new TestPageContext();
|
||||
return new WeakReference<IPageContext>(ctx);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InitializeProperties_SetsBodyAndTitle()
|
||||
{
|
||||
var details = new Details { Title = "Hello", Body = "World" };
|
||||
var vm = new DetailsViewModel(details, CreatePageContext());
|
||||
|
||||
vm.InitializeProperties();
|
||||
|
||||
Assert.AreEqual("Hello", vm.Title);
|
||||
Assert.AreEqual("World", vm.Body);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PropChanged_Body_UpdatesViewModelProperty()
|
||||
{
|
||||
var details = new Details { Title = "Initial", Body = "Initial body" };
|
||||
var vm = new DetailsViewModel(details, CreatePageContext());
|
||||
vm.InitializeProperties();
|
||||
|
||||
// Act — toolkit Details raises PropChanged synchronously on set
|
||||
details.Body = "Updated body";
|
||||
|
||||
// The property value is set synchronously in FetchProperty;
|
||||
// ApplyPendingUpdates flushes the PropertyChanged notification queue.
|
||||
vm.ApplyPendingUpdates();
|
||||
|
||||
Assert.AreEqual("Updated body", vm.Body);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PropChanged_Title_UpdatesViewModelProperty()
|
||||
{
|
||||
var details = new Details { Title = "Original", Body = "Text" };
|
||||
var vm = new DetailsViewModel(details, CreatePageContext());
|
||||
vm.InitializeProperties();
|
||||
|
||||
details.Title = "New Title";
|
||||
vm.ApplyPendingUpdates();
|
||||
|
||||
Assert.AreEqual("New Title", vm.Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PropChanged_Metadata_RebuildsList()
|
||||
{
|
||||
var details = new Details
|
||||
{
|
||||
Title = "T",
|
||||
Body = "B",
|
||||
Metadata = [],
|
||||
};
|
||||
var vm = new DetailsViewModel(details, CreatePageContext());
|
||||
vm.InitializeProperties();
|
||||
Assert.AreEqual(0, vm.Metadata.Count);
|
||||
|
||||
// Act — update metadata with a link element
|
||||
details.Metadata = [new DetailsElement { Key = "link", Data = new DetailsLink("http://example.com", "Example") }];
|
||||
vm.ApplyPendingUpdates();
|
||||
|
||||
Assert.AreEqual(1, vm.Metadata.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Cleanup_UnsubscribesFromPropChanged()
|
||||
{
|
||||
var details = new Details { Title = "T", Body = "Original" };
|
||||
var vm = new DetailsViewModel(details, CreatePageContext());
|
||||
vm.InitializeProperties();
|
||||
|
||||
// Act — cleanup unsubscribes, then change should not propagate
|
||||
vm.SafeCleanup();
|
||||
details.Body = "After cleanup";
|
||||
|
||||
Assert.AreEqual("Original", vm.Body);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NonObservableDetails_DoesNotThrow()
|
||||
{
|
||||
// IDetails that does NOT implement INotifyPropChanged
|
||||
var details = new NonObservableDetails();
|
||||
var vm = new DetailsViewModel(details, CreatePageContext());
|
||||
|
||||
// Should not throw — just doesn't subscribe to anything
|
||||
vm.InitializeProperties();
|
||||
|
||||
Assert.AreEqual("Static Title", vm.Title);
|
||||
Assert.AreEqual("Static Body", vm.Body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A minimal IDetails that does NOT implement INotifyPropChanged.
|
||||
/// </summary>
|
||||
private sealed partial class NonObservableDetails : IDetails
|
||||
{
|
||||
public IIconInfo HeroImage => new IconInfo(string.Empty);
|
||||
|
||||
public string Title => "Static Title";
|
||||
|
||||
public string Body => "Static Body";
|
||||
|
||||
public IDetailsElement[] Metadata => [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="DockSettings"/> (and its nested <see cref="DockMonitorConfig"/>)
|
||||
/// compare by <em>content</em> rather than by list reference. Dock consumers guard their
|
||||
/// (expensive) reloads with <c>_settings == args.DockSettings</c>, so two settings that differ
|
||||
/// only by having freshly-rebuilt band lists — e.g. after loading from disk — must compare
|
||||
/// equal, otherwise the reload fires needlessly.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class DockSettingsEqualityTests
|
||||
{
|
||||
private static DockBandSettings Band(string providerId, string commandId, bool? showTitles = null) =>
|
||||
new() { ProviderId = providerId, CommandId = commandId, ShowTitles = showTitles };
|
||||
|
||||
[TestMethod]
|
||||
public void Equal_WhenBandListsHaveSameContentButDifferentInstances()
|
||||
{
|
||||
var a = new DockSettings
|
||||
{
|
||||
StartBands = ImmutableList.Create(Band("p", "home"), Band("w", "winget", showTitles: false)),
|
||||
};
|
||||
var b = new DockSettings
|
||||
{
|
||||
StartBands = ImmutableList.Create(Band("p", "home"), Band("w", "winget", showTitles: false)),
|
||||
};
|
||||
|
||||
// Distinct ImmutableList instances — would be reference-unequal without EquatableList.
|
||||
Assert.AreNotSame(a.StartBands, b.StartBands);
|
||||
Assert.AreEqual(a, b);
|
||||
Assert.IsTrue(a == b);
|
||||
Assert.AreEqual(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NotEqual_WhenABandDiffers()
|
||||
{
|
||||
var a = new DockSettings { StartBands = ImmutableList.Create(Band("p", "home")) };
|
||||
var b = new DockSettings { StartBands = ImmutableList.Create(Band("p", "settings")) };
|
||||
|
||||
Assert.AreNotEqual(a, b);
|
||||
Assert.IsTrue(a != b);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NotEqual_WhenBandOrderDiffers()
|
||||
{
|
||||
var a = new DockSettings { StartBands = ImmutableList.Create(Band("p", "a"), Band("p", "b")) };
|
||||
var b = new DockSettings { StartBands = ImmutableList.Create(Band("p", "b"), Band("p", "a")) };
|
||||
|
||||
Assert.AreNotEqual(a, b);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Equal_WhenMonitorConfigsHaveSameContentIncludingPerMonitorBands()
|
||||
{
|
||||
var a = new DockSettings
|
||||
{
|
||||
MonitorConfigs = ImmutableList.Create(new DockMonitorConfig
|
||||
{
|
||||
MonitorDeviceId = @"\\.\DISPLAY1",
|
||||
IsCustomized = true,
|
||||
StartBands = ImmutableList.Create(Band("p", "home")),
|
||||
}),
|
||||
};
|
||||
var b = new DockSettings
|
||||
{
|
||||
MonitorConfigs = ImmutableList.Create(new DockMonitorConfig
|
||||
{
|
||||
MonitorDeviceId = @"\\.\DISPLAY1",
|
||||
IsCustomized = true,
|
||||
StartBands = ImmutableList.Create(Band("p", "home")),
|
||||
}),
|
||||
};
|
||||
|
||||
Assert.AreEqual(a, b);
|
||||
Assert.AreEqual(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NotEqual_WhenPerMonitorBandsIsNullVersusEmpty()
|
||||
{
|
||||
// null means "inherit global bands"; an explicit empty list does not. The two must
|
||||
// stay distinguishable so a monitor can't silently switch between the two meanings.
|
||||
var inherit = new DockMonitorConfig { MonitorDeviceId = "m", StartBands = null };
|
||||
var explicitEmpty = new DockMonitorConfig { MonitorDeviceId = "m", StartBands = ImmutableList<DockBandSettings>.Empty };
|
||||
|
||||
Assert.AreNotEqual(inherit, explicitEmpty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="EquatableList{T}"/> — the value-equality wrapper used as the backing
|
||||
/// field for record list properties so record equality compares list contents, not references.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class EquatableListTests
|
||||
{
|
||||
private static EquatableList<string> Of(params string[] items) =>
|
||||
new(ImmutableList.Create(items));
|
||||
|
||||
[TestMethod]
|
||||
public void Equal_WhenSameContentDifferentInstances()
|
||||
{
|
||||
var a = Of("x", "y", "z");
|
||||
var b = Of("x", "y", "z");
|
||||
|
||||
Assert.AreNotSame(a.List, b.List);
|
||||
Assert.IsTrue(a.Equals(b));
|
||||
Assert.AreEqual(a, b);
|
||||
Assert.AreEqual(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Equal_WhenSameUnderlyingReference()
|
||||
{
|
||||
var shared = ImmutableList.Create("a", "b");
|
||||
var a = new EquatableList<string>(shared);
|
||||
var b = new EquatableList<string>(shared);
|
||||
|
||||
Assert.IsTrue(a.Equals(b));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NotEqual_WhenAnElementDiffers()
|
||||
{
|
||||
Assert.AreNotEqual(Of("a", "b"), Of("a", "c"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NotEqual_WhenOrderDiffers()
|
||||
{
|
||||
Assert.AreNotEqual(Of("a", "b"), Of("b", "a"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NotEqual_WhenCountsDiffer()
|
||||
{
|
||||
Assert.AreNotEqual(Of("a"), Of("a", "b"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyLists_AreEqual()
|
||||
{
|
||||
var fromEmpty = new EquatableList<string>(ImmutableList<string>.Empty);
|
||||
var fromNull = new EquatableList<string>(null);
|
||||
|
||||
Assert.IsTrue(fromEmpty.Equals(fromNull));
|
||||
Assert.AreEqual(fromEmpty.GetHashCode(), fromNull.GetHashCode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Default_ExposesEmptyListAndEqualsEmpty()
|
||||
{
|
||||
EquatableList<string> defaulted = default;
|
||||
|
||||
// A default(struct) has a null inner list; List must still surface an empty list
|
||||
// (never null) and compare equal to an explicitly-empty instance.
|
||||
Assert.IsNotNull(defaulted.List);
|
||||
Assert.AreEqual(0, defaulted.List.Count);
|
||||
Assert.IsTrue(defaulted.Equals(new EquatableList<string>(ImmutableList<string>.Empty)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void List_IsNeverNull()
|
||||
{
|
||||
Assert.IsNotNull(new EquatableList<string>(null).List);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EqualsObject_ReturnsFalse_ForOtherTypes()
|
||||
{
|
||||
object other = "not an equatable list";
|
||||
|
||||
Assert.IsFalse(Of("a").Equals(other));
|
||||
Assert.IsFalse(Of("a").Equals(null!));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EqualsObject_ReturnsTrue_ForBoxedEqualValue()
|
||||
{
|
||||
object boxed = Of("a", "b");
|
||||
|
||||
Assert.IsTrue(Of("a", "b").Equals(boxed));
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,16 @@ public partial class PerformanceMonitorCommandsProvider : CommandProvider
|
||||
public const string ProviderLoadGuardBlockId = ProviderIdValue + ".ProviderLoad";
|
||||
public const string PageIdValue = "com.microsoft.cmdpal.performanceWidget";
|
||||
|
||||
private static readonly PerformanceMetricKind?[] BandMetrics =
|
||||
[
|
||||
null,
|
||||
PerformanceMetricKind.Cpu,
|
||||
PerformanceMetricKind.Memory,
|
||||
PerformanceMetricKind.Network,
|
||||
PerformanceMetricKind.Gpu,
|
||||
PerformanceMetricKind.Battery,
|
||||
];
|
||||
|
||||
internal static ProviderCrashSentinel CrashSentinel { get; } = new(ProviderIdValue);
|
||||
|
||||
private readonly Lock _stateLock = new();
|
||||
@@ -109,15 +119,26 @@ public partial class PerformanceMonitorCommandsProvider : CommandProvider
|
||||
DisposeActivePages();
|
||||
|
||||
var page = new PerformanceMonitorDisabledPage(this);
|
||||
var band = new PerformanceMonitorDisabledPage(this);
|
||||
_bands =
|
||||
[
|
||||
new CommandItem(band)
|
||||
|
||||
// Mirror the compact form of the real bands, and keep texts short
|
||||
var disabledValue = Resources.GetResource("Performance_Monitor_Disabled_Band_Title");
|
||||
var bands = new List<ICommandItem>(BandMetrics.Length);
|
||||
foreach (var metric in BandMetrics)
|
||||
{
|
||||
var icon = GetBandIcon(metric);
|
||||
var item = new ListItem(page)
|
||||
{
|
||||
Title = Resources.GetResource("Performance_Monitor_Disabled_Band_Title"),
|
||||
Subtitle = DisplayName,
|
||||
},
|
||||
];
|
||||
Title = disabledValue,
|
||||
Subtitle = GetBandSubtitle(metric),
|
||||
Icon = icon,
|
||||
};
|
||||
bands.Add(new WrappedDockItem([item], PerformanceWidgetsPage.GetBandId(metric), GetBandDisplayTitle(metric))
|
||||
{
|
||||
Icon = icon,
|
||||
});
|
||||
}
|
||||
|
||||
_bands = bands.ToArray();
|
||||
_commands =
|
||||
[
|
||||
new CommandItem(page)
|
||||
@@ -129,6 +150,45 @@ public partial class PerformanceMonitorCommandsProvider : CommandProvider
|
||||
_softDisabled = true;
|
||||
}
|
||||
|
||||
private string GetBandDisplayTitle(PerformanceMetricKind? metric)
|
||||
{
|
||||
return metric switch
|
||||
{
|
||||
PerformanceMetricKind.Cpu => Resources.GetResource("CPU_Usage_Title"),
|
||||
PerformanceMetricKind.Memory => Resources.GetResource("Memory_Usage_Title"),
|
||||
PerformanceMetricKind.Network => Resources.GetResource("Network_Usage_Title"),
|
||||
PerformanceMetricKind.Gpu => Resources.GetResource("GPU_Usage_Title"),
|
||||
PerformanceMetricKind.Battery => Resources.GetResource("Battery_Usage_Title"),
|
||||
_ => DisplayName,
|
||||
};
|
||||
}
|
||||
|
||||
private string GetBandSubtitle(PerformanceMetricKind? metric)
|
||||
{
|
||||
return metric switch
|
||||
{
|
||||
PerformanceMetricKind.Cpu => Resources.GetResource("CPU_Usage_Subtitle"),
|
||||
PerformanceMetricKind.Memory => Resources.GetResource("Memory_Usage_Subtitle"),
|
||||
PerformanceMetricKind.Network => Resources.GetResource("Network_Usage_Subtitle"),
|
||||
PerformanceMetricKind.Gpu => Resources.GetResource("GPU_Usage_Subtitle"),
|
||||
PerformanceMetricKind.Battery => Resources.GetResource("Battery_Usage_Subtitle"),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private static IconInfo GetBandIcon(PerformanceMetricKind? metric)
|
||||
{
|
||||
return metric switch
|
||||
{
|
||||
PerformanceMetricKind.Cpu => Icons.CpuIcon,
|
||||
PerformanceMetricKind.Memory => Icons.MemoryIcon,
|
||||
PerformanceMetricKind.Network => Icons.NetworkIcon,
|
||||
PerformanceMetricKind.Gpu => Icons.GpuIcon,
|
||||
PerformanceMetricKind.Battery => Icons.BatteryIcon,
|
||||
_ => Icons.PerformanceMonitorIcon,
|
||||
};
|
||||
}
|
||||
|
||||
private void SetEnabledState()
|
||||
{
|
||||
DisposeActivePages();
|
||||
|
||||
@@ -12,9 +12,9 @@ internal sealed partial class PerformanceMonitorDisabledPage : ContentPage
|
||||
{
|
||||
private readonly MarkdownContent _content;
|
||||
|
||||
public PerformanceMonitorDisabledPage(PerformanceMonitorCommandsProvider provider)
|
||||
public PerformanceMonitorDisabledPage(PerformanceMonitorCommandsProvider provider, string? id = null)
|
||||
{
|
||||
Id = PerformanceMonitorCommandsProvider.PageIdValue;
|
||||
Id = id ?? PerformanceMonitorCommandsProvider.PageIdValue;
|
||||
Name = Resources.GetResource("Performance_Monitor_Disabled_Title");
|
||||
Title = Resources.GetResource("Performance_Monitor_Title");
|
||||
Icon = Icons.PerformanceMonitorIcon;
|
||||
|
||||
@@ -93,7 +93,7 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
|
||||
{
|
||||
_isBandPage = isBandPage;
|
||||
_singleMetric = singleMetric;
|
||||
_id = singleMetric is null ? BaseId : $"{BaseId}.{GetMetricSuffix(singleMetric.Value)}";
|
||||
_id = GetBandId(singleMetric);
|
||||
|
||||
if (IncludesMetric(PerformanceMetricKind.Cpu))
|
||||
{
|
||||
@@ -287,6 +287,11 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
|
||||
_batteryPage?.Dispose();
|
||||
}
|
||||
|
||||
internal static string GetBandId(PerformanceMetricKind? metric)
|
||||
{
|
||||
return metric is null ? BaseId : $"{BaseId}.{GetMetricSuffix(metric.Value)}";
|
||||
}
|
||||
|
||||
private bool IncludesMetric(PerformanceMetricKind metric)
|
||||
{
|
||||
return _singleMetric is null || _singleMetric == metric;
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
<value>Temporarily disabled after repeated startup crashes</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Disabled_Band_Title" xml:space="preserve">
|
||||
<value>Perf disabled</value>
|
||||
<value>Disabled</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Disabled_Body" xml:space="preserve">
|
||||
<value>Performance monitor was temporarily disabled after repeated crashes while initializing or reading performance counters.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Timers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
internal sealed partial class SampleLiveDetailsPage : ListPage, IDisposable
|
||||
{
|
||||
private readonly Details _clockDetails = new()
|
||||
{
|
||||
Title = "Current Time",
|
||||
Body = "Loading...",
|
||||
};
|
||||
|
||||
private readonly Details _counterDetails = new()
|
||||
{
|
||||
Title = "Count: 0",
|
||||
Body = "Elapsed: 0 seconds",
|
||||
};
|
||||
|
||||
private readonly Details _staticDetails = new()
|
||||
{
|
||||
Title = "Static Details",
|
||||
Body = "This item does not update. Select the items above to see live updates in the details pane.",
|
||||
};
|
||||
|
||||
private readonly ListItem[] _items;
|
||||
private Timer? _timer;
|
||||
private int _counter;
|
||||
private bool _disposed;
|
||||
|
||||
public SampleLiveDetailsPage()
|
||||
{
|
||||
Icon = new IconInfo("\uE916"); // Refresh
|
||||
Name = Title = "Live Updating Details";
|
||||
ShowDetails = true;
|
||||
|
||||
_items = [
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Live Clock",
|
||||
Subtitle = "Details pane shows current time, updating every second",
|
||||
Details = _clockDetails,
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Counter",
|
||||
Subtitle = "Details pane increments a counter every second",
|
||||
Details = _counterDetails,
|
||||
},
|
||||
new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = "Static Item",
|
||||
Subtitle = "This item's details do not change",
|
||||
Details = _staticDetails,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
if (_timer is null)
|
||||
{
|
||||
_timer = new Timer(1000);
|
||||
_timer.Elapsed += Timer_Elapsed;
|
||||
_timer.AutoReset = true;
|
||||
_timer.Enabled = true;
|
||||
}
|
||||
|
||||
return _items;
|
||||
}
|
||||
|
||||
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
_counter++;
|
||||
|
||||
// Updating Details properties fires INotifyPropChanged automatically
|
||||
// (Details extends BaseObservable). DetailsViewModel picks up the change
|
||||
// live without requiring the user to reselect the item.
|
||||
_clockDetails.Body = DateTime.Now.ToString("HH:mm:ss", CultureInfo.CurrentCulture);
|
||||
|
||||
_counterDetails.Title = $"Count: {_counter}";
|
||||
_counterDetails.Body = $"Elapsed: {_counter} second{(_counter == 1 ? string.Empty : "s")}";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,11 @@ public partial class SamplesListPage : ListPage
|
||||
Title = "List Page With Details",
|
||||
Subtitle = "A list of items, each with additional details to display",
|
||||
},
|
||||
new ListItem(new SampleLiveDetailsPage())
|
||||
{
|
||||
Title = "Live Updating Details",
|
||||
Subtitle = "Details pane updates in real time without reselecting",
|
||||
},
|
||||
new ListItem(new SectionsIndexPage())
|
||||
{
|
||||
Title = "List Pages With Sections",
|
||||
|
||||
Reference in New Issue
Block a user