From a50d548a0716647d46dab6d8d38253224acaa328 Mon Sep 17 00:00:00 2001 From: Davide Giacometti <25966642+davidegiacometti@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:22:15 +0200 Subject: [PATCH] [QuickAccent] Persist characters usage between runs (#37577) ## Summary of the Pull Request ## PR Checklist Persist characters usage between PowerToys/QuickAccent runs. - [x] **Closes:** #26034 - [ ] **Communication:** I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end user facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments - Persist the dictionaries used to determine the characters usage in a JSON file `%LOCALAPPDATA%\Microsoft\PowerToys\QuickAccent\UsageInfo.json` ## Validation Steps Performed Manually tested: - JSON is saved when PowerToys is closed and the **Sort characters by usage frequency** is on - JSON is deleted when QuickAccent is called and **Sort characters by usage frequency** is off - JSON is read when QuickAccent is started and characters order is applied from the previous run --------- Co-authored-by: Gleb Khmyznikov Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../PowerAccent.Core/Models/UsageInfoData.cs | 13 ++++ .../PowerAccent.Core/PowerAccent.cs | 12 ++- .../SourceGenerationContext.cs | 2 + .../Tools/CharactersUsageInfo.cs | 76 ++++++++++++++++++- .../PowerAccent.UI/Selector.xaml.cs | 1 + 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 src/modules/poweraccent/PowerAccent.Core/Models/UsageInfoData.cs diff --git a/src/modules/poweraccent/PowerAccent.Core/Models/UsageInfoData.cs b/src/modules/poweraccent/PowerAccent.Core/Models/UsageInfoData.cs new file mode 100644 index 0000000000..322e4eae79 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Models/UsageInfoData.cs @@ -0,0 +1,13 @@ +// 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. + +namespace PowerAccent.Core.Models +{ + public class UsageInfoData + { + public Dictionary CharacterUsageCounters { get; set; } = []; + + public Dictionary CharacterUsageTimestamp { get; set; } = []; + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs index c2f0698f25..fa020ee4fe 100644 --- a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs +++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs @@ -117,8 +117,8 @@ public partial class PowerAccent : IDisposable if (_settingService.SortByUsageFrequency) { characters = characters.OrderByDescending(character => _usageInfo.GetUsageFrequency(character)) - .ThenByDescending(character => _usageInfo.GetLastUsageTimestamp(character)). - ToArray(); + .ThenByDescending(character => _usageInfo.GetLastUsageTimestamp(character)) + .ToArray(); } else if (!_usageInfo.Empty()) { @@ -337,6 +337,14 @@ public partial class PowerAccent : IDisposable return _settingService.Position; } + public void SaveUsageInfo() + { + if (_settingService.SortByUsageFrequency) + { + _usageInfo.Save(); + } + } + public void Dispose() { _keyboardListener.UnInitHook(); diff --git a/src/modules/poweraccent/PowerAccent.Core/SerializationContext/SourceGenerationContext.cs b/src/modules/poweraccent/PowerAccent.Core/SerializationContext/SourceGenerationContext.cs index e682aa7b63..55fdd144a1 100644 --- a/src/modules/poweraccent/PowerAccent.Core/SerializationContext/SourceGenerationContext.cs +++ b/src/modules/poweraccent/PowerAccent.Core/SerializationContext/SourceGenerationContext.cs @@ -3,12 +3,14 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; +using PowerAccent.Core.Models; using PowerAccent.Core.Services; namespace PowerAccent.Core.SerializationContext; [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(SettingsService))] +[JsonSerializable(typeof(UsageInfoData))] public partial class SourceGenerationContext : JsonSerializerContext { } diff --git a/src/modules/poweraccent/PowerAccent.Core/Tools/CharactersUsageInfo.cs b/src/modules/poweraccent/PowerAccent.Core/Tools/CharactersUsageInfo.cs index 07a448bbe0..492a9828d9 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Tools/CharactersUsageInfo.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Tools/CharactersUsageInfo.cs @@ -2,12 +2,28 @@ // 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.IO; +using System.Text.Json; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using PowerAccent.Core.Models; +using PowerAccent.Core.SerializationContext; + namespace PowerAccent.Core.Tools { public class CharactersUsageInfo { - private Dictionary _characterUsageCounters = new Dictionary(); - private Dictionary _characterUsageTimestamp = new Dictionary(); + private readonly string _filePath; + private readonly Dictionary _characterUsageCounters; + private readonly Dictionary _characterUsageTimestamp; + + public CharactersUsageInfo() + { + _filePath = new SettingsUtils().GetSettingsFilePath(PowerAccentSettings.ModuleName, "UsageInfo.json"); + var data = GetUsageInfoData(); + _characterUsageCounters = data.CharacterUsageCounters; + _characterUsageTimestamp = data.CharacterUsageTimestamp; + } public bool Empty() { @@ -18,19 +34,18 @@ namespace PowerAccent.Core.Tools { _characterUsageCounters.Clear(); _characterUsageTimestamp.Clear(); + Delete(); } public uint GetUsageFrequency(string character) { _characterUsageCounters.TryGetValue(character, out uint frequency); - return frequency; } public long GetLastUsageTimestamp(string character) { _characterUsageTimestamp.TryGetValue(character, out long timestamp); - return timestamp; } @@ -47,5 +62,58 @@ namespace PowerAccent.Core.Tools _characterUsageTimestamp[character] = DateTimeOffset.Now.ToUnixTimeSeconds(); } + + public void Save() + { + var data = new UsageInfoData + { + CharacterUsageCounters = _characterUsageCounters, + CharacterUsageTimestamp = _characterUsageTimestamp, + }; + + try + { + var json = JsonSerializer.Serialize(data, SourceGenerationContext.Default.UsageInfoData); + File.WriteAllText(_filePath, json); + } + catch (Exception ex) + { + Logger.LogError("Failed to save usage file", ex); + } + } + + public void Delete() + { + try + { + if (File.Exists(_filePath)) + { + File.Delete(_filePath); + } + } + catch (Exception ex) + { + Logger.LogError("Failed to delete usage file", ex); + } + } + + private UsageInfoData GetUsageInfoData() + { + if (!File.Exists(_filePath)) + { + return new UsageInfoData(); + } + + try + { + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize(json, SourceGenerationContext.Default.UsageInfoData) ?? new UsageInfoData(); + } + catch (Exception ex) + { + Logger.LogError("Failed to read usage file", ex); + return new UsageInfoData(); + } + } } } diff --git a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs index 449aae94db..311417851a 100644 --- a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs +++ b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs @@ -98,6 +98,7 @@ public partial class Selector : FluentWindow, IDisposable, INotifyPropertyChange protected override void OnClosed(EventArgs e) { + _powerAccent.SaveUsageInfo(); _powerAccent.Dispose(); base.OnClosed(e); }