diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs index d7ae386191..8461b4a6d8 100644 --- a/src/common/ManagedCommon/ModuleType.cs +++ b/src/common/ManagedCommon/ModuleType.cs @@ -36,5 +36,6 @@ namespace ManagedCommon PowerOCR, Workspaces, ZoomIt, + GeneralSettings, } } diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp index de7703d99e..9c8c2bf580 100644 --- a/src/runner/general_settings.cpp +++ b/src/runner/general_settings.cpp @@ -189,6 +189,12 @@ GeneralSettings get_general_settings() void apply_general_settings(const json::JsonObject& general_configs, bool save) { + std::wstring old_settings_json_string; + if (save) + { + old_settings_json_string = get_general_settings().to_json().Stringify().c_str(); + } + Logger::info(L"apply_general_settings: {}", std::wstring{ general_configs.ToString() }); run_as_elevated = general_configs.GetNamedBoolean(L"run_elevated", false); @@ -355,8 +361,12 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save) if (save) { GeneralSettings save_settings = get_general_settings(); - PTSettingsHelper::save_general_settings(save_settings.to_json()); - Trace::SettingsChanged(save_settings); + std::wstring new_settings_json_string = save_settings.to_json().Stringify().c_str(); + if (old_settings_json_string != new_settings_json_string) + { + PTSettingsHelper::save_general_settings(save_settings.to_json()); + Trace::SettingsChanged(save_settings); + } } } diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index 48625f2b78..ba476b6c98 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -190,12 +190,12 @@ void dispatch_received_json(const std::wstring& json_to_parse) if (name == L"general") { apply_general_settings(value.GetObjectW()); - const std::wstring settings_string{ get_all_settings().Stringify().c_str() }; - { - std::unique_lock lock{ ipc_mutex }; - if (current_settings_ipc) - current_settings_ipc->send(settings_string); - } + // const std::wstring settings_string{ get_all_settings().Stringify().c_str() }; + // { + // std::unique_lock lock{ ipc_mutex }; + // if (current_settings_ipc) + // current_settings_ipc->send(settings_string); + // } } else if (name == L"powertoys") { diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp index 77ceaccb48..92b723c9cb 100644 --- a/src/runner/tray_icon.cpp +++ b/src/runner/tray_icon.cpp @@ -6,6 +6,7 @@ #include "centralized_hotkeys.h" #include "centralized_kb_hook.h" #include "quick_access_host.h" +#include "hotkey_conflict_detector.h" #include #include @@ -357,18 +358,31 @@ void update_quick_access_hotkey(bool enabled, PowerToysSettings::HotkeyObject ho { static PowerToysSettings::HotkeyObject current_hotkey; static bool is_registered = false; + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); if (is_registered) { - CentralizedHotkeys::UnregisterHotkeysForModule(L"QuickAccess"); + CentralizedKeyboardHook::ClearModuleHotkeys(L"QuickAccess"); + hkmng.RemoveHotkeyByModule(L"GeneralSettings"); is_registered = false; } if (enabled && hotkey.get_code() != 0) { - CentralizedHotkeys::AddHotkeyAction({ static_cast(hotkey.get_modifiers()), static_cast(hotkey.get_code()) }, { L"QuickAccess", [](WORD, WORD) { + HotkeyConflictDetector::Hotkey hk = { + hotkey.win_pressed(), + hotkey.ctrl_pressed(), + hotkey.shift_pressed(), + hotkey.alt_pressed(), + static_cast(hotkey.get_code()) + }; + + hkmng.AddHotkey(hk, L"GeneralSettings", 0, true); + CentralizedKeyboardHook::SetHotkeyAction(L"QuickAccess", hk, []() { open_quick_access_flyout_window(); - }}); + return true; + }); + current_hotkey = hotkey; is_registered = true; } diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml index 9ac2ba16a5..ab7b3c250a 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml @@ -8,10 +8,9 @@ - - - - + + + @@ -20,21 +19,29 @@ x:Key="LayerOnAcrylicFillColorDefaultBrush" Opacity="0.7" Color="#FFFFFFFF" /> + + + + + + + + diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs index e6b60225e2..4d86dffcd4 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs @@ -372,7 +372,7 @@ public sealed partial class MainWindow : WindowEx, IDisposable _exitEvent?.Dispose(); _exitEvent = null; - if (_hwnd != IntPtr.Zero) + if (_hwnd != IntPtr.Zero && IsWindow(_hwnd)) { UncloakWindow(); } @@ -385,6 +385,10 @@ public sealed partial class MainWindow : WindowEx, IDisposable _disposed = true; } + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsWindow(IntPtr hWnd); + [DllImport("user32.dll", EntryPoint = "ShowWindow", SetLastError = true)] private static extern bool ShowWindowNative(IntPtr hWnd, int nCmdShow); diff --git a/src/settings-ui/QuickAccess.UI/ViewModels/AllAppsViewModel.cs b/src/settings-ui/QuickAccess.UI/ViewModels/AllAppsViewModel.cs index 3d2ead130e..affd3868d9 100644 --- a/src/settings-ui/QuickAccess.UI/ViewModels/AllAppsViewModel.cs +++ b/src/settings-ui/QuickAccess.UI/ViewModels/AllAppsViewModel.cs @@ -86,6 +86,11 @@ public sealed class AllAppsViewModel : Observable foreach (ModuleType moduleType in Enum.GetValues()) { + if (moduleType == ModuleType.GeneralSettings) + { + continue; + } + var gpo = ModuleHelper.GetModuleGpoConfiguration(moduleType); var isLocked = gpo is GpoRuleConfigured.Enabled or GpoRuleConfigured.Disabled; var isEnabled = gpo == GpoRuleConfigured.Enabled || (!isLocked && ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType)); diff --git a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs index 9d640a10b7..b02f28ff13 100644 --- a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs +++ b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Settings.UI.Library.Attributes; @@ -19,7 +20,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library ByStatus, } - public class GeneralSettings : ISettingsConfig + public class GeneralSettings : ISettingsConfig, IHotkeyConfig { // Gets or sets a value indicating whether run powertoys on start-up. [JsonPropertyName("startup")] @@ -126,6 +127,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library IgnoredConflictProperties = new ShortcutConflictProperties(); } + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + return new HotkeyAccessor[] + { + new HotkeyAccessor( + () => QuickAccessShortcut, + (hotkey) => { QuickAccessShortcut = hotkey; }, + "GeneralPage_QuickAccessShortcut"), + }; + } + + public ModuleType GetModuleType() + { + return ModuleType.GeneralSettings; + } + // converts the current to a json string. public string ToJsonString() { diff --git a/src/settings-ui/Settings.UI.Library/SettingsFactory.cs b/src/settings-ui/Settings.UI.Library/SettingsFactory.cs index 2bb9e79121..5ca9457fef 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsFactory.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsFactory.cs @@ -68,6 +68,11 @@ namespace Microsoft.PowerToys.Settings.UI.Services if (settingsInstance != null) { var moduleName = settingsInstance.GetModuleName(); + if (string.IsNullOrEmpty(moduleName) && type == typeof(GeneralSettings)) + { + moduleName = "GeneralSettings"; + } + if (!string.IsNullOrEmpty(moduleName)) { settingsTypes[moduleName] = type; @@ -104,7 +109,13 @@ namespace Microsoft.PowerToys.Settings.UI.Services var genericMethod = getSettingsMethod?.MakeGenericMethod(settingsType); // Call GetSettingsOrDefault(moduleKey) to get fresh settings from file - var freshSettings = genericMethod?.Invoke(_settingsUtils, new object[] { moduleKey, "settings.json" }); + string actualModuleKey = moduleKey; + if (moduleKey == "GeneralSettings") + { + actualModuleKey = string.Empty; + } + + var freshSettings = genericMethod?.Invoke(_settingsUtils, new object[] { actualModuleKey, "settings.json" }); return freshSettings as IHotkeyConfig; } diff --git a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs index 89b22d8164..b4c4a379ee 100644 --- a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs +++ b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs @@ -24,6 +24,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.MouseJump: case ModuleType.MousePointerCrosshairs: case ModuleType.CursorWrap: return $"MouseUtils_{moduleType}/Header"; + case ModuleType.GeneralSettings: return "QuickAccessTitle/Title"; default: return $"{moduleType}/ModuleTitle"; } } @@ -39,6 +40,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.MousePointerCrosshairs: return "ms-appx:///Assets/Settings/Icons/MouseCrosshairs.png"; case ModuleType.MeasureTool: return "ms-appx:///Assets/Settings/Icons/ScreenRuler.png"; case ModuleType.PowerLauncher: return $"ms-appx:///Assets/Settings/Icons/PowerToysRun.png"; + case ModuleType.GeneralSettings: return "ms-appx:///Assets/Settings/Icons/PowerToys.png"; default: return $"ms-appx:///Assets/Settings/Icons/{moduleType}.png"; } } @@ -77,6 +79,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.ShortcutGuide: return generalSettingsConfig.Enabled.ShortcutGuide; case ModuleType.PowerOCR: return generalSettingsConfig.Enabled.PowerOcr; case ModuleType.ZoomIt: return generalSettingsConfig.Enabled.ZoomIt; + case ModuleType.GeneralSettings: return generalSettingsConfig.EnableQuickAccess; default: return false; } } @@ -115,6 +118,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.ShortcutGuide: generalSettingsConfig.Enabled.ShortcutGuide = isEnabled; break; case ModuleType.PowerOCR: generalSettingsConfig.Enabled.PowerOcr = isEnabled; break; case ModuleType.ZoomIt: generalSettingsConfig.Enabled.ZoomIt = isEnabled; break; + case ModuleType.GeneralSettings: generalSettingsConfig.EnableQuickAccess = isEnabled; break; } } @@ -171,6 +175,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers ModuleType.FancyZones => typeof(FancyZonesPage), ModuleType.FileLocksmith => typeof(FileLocksmithPage), ModuleType.FindMyMouse => typeof(MouseUtilsPage), + ModuleType.GeneralSettings => typeof(GeneralPage), ModuleType.Hosts => typeof(HostsPage), ModuleType.ImageResizer => typeof(ImageResizerPage), ModuleType.KeyboardManager => typeof(KeyboardManagerPage), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs index 3d0724f83e..8286e16888 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs @@ -93,6 +93,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views CheckBugReportStatus(); doRefreshBackupRestoreStatus(100); + + this.Loaded += (s, e) => ViewModel.OnPageLoaded(); } private void OpenColorsSettings_Click(object sender, RoutedEventArgs e) diff --git a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs index 537acbf7de..40070a4a71 100644 --- a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs @@ -48,6 +48,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels // Flag to prevent circular updates when a UI toggle triggers settings changes. private bool _isUpdatingFromUI; + private bool _isUpdatingFromSettings; private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData(); @@ -164,6 +165,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels foreach (ModuleType moduleType in Enum.GetValues()) { + if (moduleType == ModuleType.GeneralSettings) + { + continue; + } + GpoRuleConfigured gpo = ModuleHelper.GetModuleGpoConfiguration(moduleType); var newItem = new DashboardListItem() { @@ -263,6 +269,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels /// private void EnabledChangedOnUI(DashboardListItem dashboardListItem) { + if (_isUpdatingFromSettings) + { + return; + } + _isUpdatingFromUI = true; try { @@ -306,6 +317,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels return; } + _isUpdatingFromSettings = true; try { RefreshModuleList(); @@ -320,6 +332,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { Logger.LogError($"Updating active/disabled modules list failed: {ex.Message}"); } + finally + { + _isUpdatingFromSettings = false; + } } /// @@ -731,5 +747,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels NavigationService.Navigate(ModuleHelper.GetModulePageType(moduleType)); } } + + public override void Dispose() + { + base.Dispose(); + if (_settingsRepository != null) + { + _settingsRepository.SettingsChanged -= OnSettingsChanged; + } + + GC.SuppressFinalize(this); + } } } diff --git a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs index 05a4866784..aa11e6bf06 100644 --- a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs @@ -31,7 +31,7 @@ using Windows.System.Profile; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class GeneralViewModel : Observable + public partial class GeneralViewModel : PageViewModelBase { public enum InstallScope { @@ -39,6 +39,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels PerUser, } + protected override string ModuleName => "GeneralSettings"; + + public override Dictionary GetAllHotkeySettings() + { + return new Dictionary + { + { ModuleName, new HotkeySettings[] { QuickAccessShortcut } }, + }; + } + private GeneralSettings GeneralSettingsConfig { get; set; } private UpdatingSettings UpdatingSettingsConfig { get; set; } @@ -75,6 +85,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private string _settingsConfigFileFolder = string.Empty; + private ISettingsRepository _settingsRepository; + private Microsoft.UI.Dispatching.DispatcherQueue _dispatcherQueue; + private IFileSystemWatcher _fileWatcher; private Func> PickSingleFolderDialog { get; } @@ -100,6 +113,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels // To obtain the general settings configuration of PowerToys if it exists, else to create a new file and return the default configurations. ArgumentNullException.ThrowIfNull(settingsRepository); + _settingsRepository = settingsRepository; + _settingsRepository.SettingsChanged += OnSettingsChanged; + _dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + GeneralSettingsConfig = settingsRepository.SettingsConfig; UpdatingSettingsConfig = UpdatingSettings.LoadSettings(); if (UpdatingSettingsConfig == null) @@ -1493,5 +1510,30 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels Process.Start("explorer.exe", etwDirPath); } } + + private void OnSettingsChanged(GeneralSettings newSettings) + { + _dispatcherQueue?.TryEnqueue(() => + { + GeneralSettingsConfig = newSettings; + + if (_enableQuickAccess != newSettings.EnableQuickAccess) + { + _enableQuickAccess = newSettings.EnableQuickAccess; + OnPropertyChanged(nameof(EnableQuickAccess)); + } + }); + } + + public override void Dispose() + { + base.Dispose(); + if (_settingsRepository != null) + { + _settingsRepository.SettingsChanged -= OnSettingsChanged; + } + + GC.SuppressFinalize(this); + } } }