diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index 53cae3bac3e..5b656930830 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -316,7 +316,7 @@ BOOL run_settings_non_elevated(LPCWSTR executable_path, LPWSTR executable_args, DWORD g_settings_process_id = 0; -void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::optional settings_window, bool show_flyout = false) +void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::optional settings_window, bool show_flyout = false, const std::optional& flyout_position = std::nullopt) { g_isLaunchInProgress = true; @@ -389,21 +389,31 @@ void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::op // Arg 10: should flyout be shown std::wstring settings_showFlyout = show_flyout ? L"true" : L"false"; + // Arg 11: contains if there's a settings window argument. If true, will add one extra argument with the value to the call. + std::wstring settings_containsSettingsWindow = settings_window.has_value() ? L"true" : L"false"; + + // Arg 12: contains if there's flyout coordinates. If true, will add two extra arguments to the call containing the x and y coordinates. + std::wstring settings_containsFlyoutPosition = flyout_position.has_value() ? L"true" : L"false"; + + // Args 13, .... : Optional arguments depending on the options presented before. All by the same value. + // create general settings file to initialize the settings file with installation configurations like : // 1. Run on start up. PTSettingsHelper::save_general_settings(save_settings.to_json()); - std::wstring executable_args = fmt::format(L"\"{}\" {} {} {} {} {} {} {} {} {}", - executable_path, - powertoys_pipe_name, - settings_pipe_name, - std::to_wstring(powertoys_pid), - settings_theme, - settings_elevatedStatus, - settings_isUserAnAdmin, - settings_showOobe, - settings_showScoobe, - settings_showFlyout); + std::wstring executable_args = fmt::format(L"\"{}\" {} {} {} {} {} {} {} {} {} {} {}", + executable_path, + powertoys_pipe_name, + settings_pipe_name, + std::to_wstring(powertoys_pid), + settings_theme, + settings_elevatedStatus, + settings_isUserAnAdmin, + settings_showOobe, + settings_showScoobe, + settings_showFlyout, + settings_containsSettingsWindow, + settings_containsFlyoutPosition); if (settings_window.has_value()) { @@ -411,6 +421,14 @@ void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::op executable_args.append(settings_window.value()); } + if (flyout_position) + { + executable_args.append(L" "); + executable_args.append(std::to_wstring(flyout_position.value().x)); + executable_args.append(L" "); + executable_args.append(std::to_wstring(flyout_position.value().y)); + } + BOOL process_created = false; // Commented out to fix #22659 @@ -550,7 +568,7 @@ void bring_settings_to_front() EnumWindows(callback, 0); } -void open_settings_window(std::optional settings_window, bool show_flyout = false) +void open_settings_window(std::optional settings_window, bool show_flyout = false, const std::optional& flyout_position) { if (g_settings_process_id != 0) { @@ -558,7 +576,14 @@ void open_settings_window(std::optional settings_window, bool show { if (current_settings_ipc) { - current_settings_ipc->send(L"{\"ShowYourself\":\"flyout\"}"); + if (!flyout_position.has_value()) + { + current_settings_ipc->send(L"{\"ShowYourself\":\"flyout\"}"); + } + else + { + current_settings_ipc->send(fmt::format(L"{{\"ShowYourself\":\"flyout\", \"x_position\":{}, \"y_position\":{} }}", std::to_wstring(flyout_position.value().x), std::to_wstring(flyout_position.value().y))); + } } } else @@ -575,8 +600,8 @@ void open_settings_window(std::optional settings_window, bool show { if (!g_isLaunchInProgress) { - std::thread([settings_window, show_flyout]() { - run_settings_window(false, false, settings_window, show_flyout); + std::thread([settings_window, show_flyout, flyout_position]() { + run_settings_window(false, false, settings_window, show_flyout, flyout_position); }).detach(); } } @@ -608,13 +633,6 @@ void open_scoobe_window() }).detach(); } -void open_flyout() -{ - std::thread([]() { - run_settings_window(false, false, std::nullopt, true); - }).detach(); -} - std::string ESettingsWindowNames_to_string(ESettingsWindowNames value) { switch (value) diff --git a/src/runner/settings_window.h b/src/runner/settings_window.h index fcbb51b3107..e03a7c0ef0e 100644 --- a/src/runner/settings_window.h +++ b/src/runner/settings_window.h @@ -22,7 +22,7 @@ enum class ESettingsWindowNames std::string ESettingsWindowNames_to_string(ESettingsWindowNames value); ESettingsWindowNames ESettingsWindowNames_from_string(std::string value); -void open_settings_window(std::optional settings_window, bool show_flyout); +void open_settings_window(std::optional settings_window, bool show_flyout, const std::optional& flyout_position); void close_settings_window(); void open_oobe_window(); diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp index 1ece986df74..d29527bc721 100644 --- a/src/runner/tray_icon.cpp +++ b/src/runner/tray_icon.cpp @@ -32,6 +32,8 @@ namespace HMENU h_sub_menu = nullptr; bool double_click_timer_running = false; bool double_clicked = false; + POINT tray_icon_click_point; + } // Struct to fill with callback and the data. The window_proc is responsible for cleaning it. @@ -123,7 +125,7 @@ void click_timer_elapsed() double_click_timer_running = false; if (!double_clicked) { - open_settings_window(std::nullopt, true); + open_settings_window(std::nullopt, true, tray_icon_click_point); } } @@ -212,6 +214,9 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam // ignore event if this is the second click of a double click if (!double_click_timer_running) { + // save the cursor position for sending where to show the popup. + GetCursorPos(&tray_icon_click_point); + // start timer for detecting single or double click double_click_timer_running = true; double_clicked = false; diff --git a/src/runner/tray_icon.h b/src/runner/tray_icon.h index 0d63475d21a..812f1f1c208 100644 --- a/src/runner/tray_icon.h +++ b/src/runner/tray_icon.h @@ -7,7 +7,7 @@ void start_tray_icon(); // Stop the Tray Icon void stop_tray_icon(); // Open the Settings Window -void open_settings_window(std::optional settings_window, bool show_flyout); +void open_settings_window(std::optional settings_window, bool show_flyout, const std::optional& flyout_position = std::nullopt); // Callback type to be called by the tray icon loop typedef void (*main_loop_callback_function)(PVOID); // Calls a callback in _callback diff --git a/src/settings-ui/Settings.UI/App.xaml.cs b/src/settings-ui/Settings.UI/App.xaml.cs index d07bc645d30..41486f5d17c 100644 --- a/src/settings-ui/Settings.UI/App.xaml.cs +++ b/src/settings-ui/Settings.UI/App.xaml.cs @@ -37,12 +37,12 @@ private enum Arguments ShowOobeWindow, ShowScoobeWindow, ShowFlyout, - SettingsWindow, + ContainsSettingsWindow, + ContainsFlyoutPosition, } // Quantity of arguments - private const int RequiredArgumentsQty = 10; - private const int RequiredAndOptionalArgumentsQty = 11; + private const int RequiredArgumentsQty = 12; // Create an instance of the IPC wrapper. private static TwoWayPipeMessageIPCManaged ipcmanager; @@ -122,11 +122,16 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar ShowOobe = cmdArgs[(int)Arguments.ShowOobeWindow] == "true"; ShowScoobe = cmdArgs[(int)Arguments.ShowScoobeWindow] == "true"; ShowFlyout = cmdArgs[(int)Arguments.ShowFlyout] == "true"; + bool containsSettingsWindow = cmdArgs[(int)Arguments.ContainsSettingsWindow] == "true"; + bool containsFlyoutPosition = cmdArgs[(int)Arguments.ContainsFlyoutPosition] == "true"; - if (cmdArgs.Length == RequiredAndOptionalArgumentsQty) + // To keep track of variable arguments + int currentArgumentIndex = RequiredArgumentsQty; + + if (containsSettingsWindow) { // open specific window - switch (cmdArgs[(int)Arguments.SettingsWindow]) + switch (cmdArgs[currentArgumentIndex]) { case "Overview": StartupPage = typeof(Views.GeneralPage); break; case "AlwaysOnTop": StartupPage = typeof(Views.AlwaysOnTopPage); break; @@ -148,6 +153,17 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar case "Hosts": StartupPage = typeof(Views.HostsPage); break; default: Debug.Assert(false, "Unexpected SettingsWindow argument value"); break; } + + currentArgumentIndex++; + } + + int flyout_x = 0; + int flyout_y = 0; + if (containsFlyoutPosition) + { + // get the flyout position arguments + int.TryParse(cmdArgs[currentArgumentIndex++], out flyout_x); + int.TryParse(cmdArgs[currentArgumentIndex++], out flyout_y); } RunnerHelper.WaitForPowerToysRunner(PowerToysPID, () => @@ -193,7 +209,13 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar } else if (ShowFlyout) { - ShellPage.OpenFlyoutCallback(); + POINT? p = null; + if (containsFlyoutPosition) + { + p = new POINT(flyout_x, flyout_y); + } + + ShellPage.OpenFlyoutCallback(p); } } } diff --git a/src/settings-ui/Settings.UI/FlyoutWindow.xaml.cs b/src/settings-ui/Settings.UI/FlyoutWindow.xaml.cs index aa2ab398498..c8165610343 100644 --- a/src/settings-ui/Settings.UI/FlyoutWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/FlyoutWindow.xaml.cs @@ -1,11 +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. +using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; using Microsoft.PowerToys.Settings.UI.ViewModels.Flyout; using Microsoft.PowerToys.Telemetry; using Microsoft.UI; using Microsoft.UI.Windowing; +using Windows.Graphics; using WinUIEx; namespace Microsoft.PowerToys.Settings.UI @@ -21,23 +23,65 @@ public sealed partial class FlyoutWindow : WindowEx public FlyoutViewModel ViewModel { get; set; } - public FlyoutWindow() + public POINT? FlyoutAppearPosition { get; set; } + + public FlyoutWindow(POINT? initialPosition) { this.InitializeComponent(); this.Activated += FlyoutWindow_Activated; - var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); - WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); - DisplayArea displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest); - double dpiScale = (float)this.GetDpiForWindow() / 96; - double x = displayArea.WorkArea.Width - (dpiScale * (WindowWidth + WindowMargin)); - double y = displayArea.WorkArea.Height - (dpiScale * (WindowHeight + WindowMargin)); - this.MoveAndResize(x, y, WindowWidth, WindowHeight); + FlyoutAppearPosition = initialPosition; ViewModel = new FlyoutViewModel(); } private void FlyoutWindow_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args) { PowerToysTelemetry.Log.WriteEvent(new TrayFlyoutActivatedEvent()); + if (args.WindowActivationState == Microsoft.UI.Xaml.WindowActivationState.CodeActivated) + { + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); + if (!FlyoutAppearPosition.HasValue) + { + DisplayArea displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest); + double dpiScale = (float)this.GetDpiForWindow() / 96; + double x = displayArea.WorkArea.Width - (dpiScale * (WindowWidth + WindowMargin)); + double y = displayArea.WorkArea.Height - (dpiScale * (WindowHeight + WindowMargin)); + this.MoveAndResize(x, y, WindowWidth, WindowHeight); + } + else + { + DisplayArea displayArea = DisplayArea.GetFromPoint(new PointInt32(FlyoutAppearPosition.Value.X, FlyoutAppearPosition.Value.Y), DisplayAreaFallback.Nearest); + + // Move the window to the correct screen as a little blob, so we can get the accurate dpi for the screen to calculate the best position to show it. + this.MoveAndResize(FlyoutAppearPosition.Value.X, FlyoutAppearPosition.Value.Y, 1, 1); + double dpiScale = (float)this.GetDpiForWindow() / 96; + + // Position the window so that it's inside the display are closest to the point. + POINT newPosition = new POINT(FlyoutAppearPosition.Value.X - (int)(dpiScale * WindowWidth / 2), FlyoutAppearPosition.Value.Y - (int)(dpiScale * WindowHeight / 2)); + if (newPosition.X < displayArea.WorkArea.X) + { + newPosition.X = displayArea.WorkArea.X; + } + + if (newPosition.Y < displayArea.WorkArea.Y) + { + newPosition.Y = displayArea.WorkArea.Y; + } + + if (newPosition.X + (dpiScale * WindowWidth) > displayArea.WorkArea.X + displayArea.WorkArea.Width) + { + newPosition.X = (int)(displayArea.WorkArea.X + displayArea.WorkArea.Width - (dpiScale * WindowWidth)); + } + + if (newPosition.Y + (dpiScale * WindowHeight) > displayArea.WorkArea.Y + displayArea.WorkArea.Height) + { + newPosition.Y = (int)(displayArea.WorkArea.Y + displayArea.WorkArea.Height - (dpiScale * WindowHeight)); + } + + this.MoveAndResize(newPosition.X, newPosition.Y, WindowWidth, WindowHeight); + } + } + if (args.WindowActivationState == Microsoft.UI.Xaml.WindowActivationState.Deactivated) { if (ViewModel.CanHide) diff --git a/src/settings-ui/Settings.UI/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/MainWindow.xaml.cs index 54cc7430d36..4fef64531c4 100644 --- a/src/settings-ui/Settings.UI/MainWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/MainWindow.xaml.cs @@ -171,16 +171,17 @@ public MainWindow(bool isDark, bool createHidden = false) }); // open flyout - ShellPage.SetOpenFlyoutCallback(() => + ShellPage.SetOpenFlyoutCallback((POINT? p) => { this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => { if (App.GetFlyoutWindow() == null) { - App.SetFlyoutWindow(new FlyoutWindow()); + App.SetFlyoutWindow(new FlyoutWindow(p)); } FlyoutWindow flyout = App.GetFlyoutWindow(); + flyout.FlyoutAppearPosition = p; flyout.Activate(); // https://github.com/microsoft/microsoft-ui-xaml/issues/7595 - Activate doesn't bring window to the foreground @@ -196,7 +197,7 @@ public MainWindow(bool isDark, bool createHidden = false) { if (App.GetFlyoutWindow() == null) { - App.SetFlyoutWindow(new FlyoutWindow()); + App.SetFlyoutWindow(new FlyoutWindow(null)); } App.GetFlyoutWindow().ViewModel.DisableHiding(); diff --git a/src/settings-ui/Settings.UI/Views/ShellPage.xaml.cs b/src/settings-ui/Settings.UI/Views/ShellPage.xaml.cs index 96bf5698d53..fdaf5dec218 100644 --- a/src/settings-ui/Settings.UI/Views/ShellPage.xaml.cs +++ b/src/settings-ui/Settings.UI/Views/ShellPage.xaml.cs @@ -45,7 +45,7 @@ public sealed partial class ShellPage : UserControl /// /// Declaration for the opening flyout window callback function. /// - public delegate void FlyoutOpeningCallback(); + public delegate void FlyoutOpeningCallback(POINT? point); /// /// Declaration for the disabling hide of flyout window callback function. @@ -330,13 +330,28 @@ private void ReceiveMessage(JsonObject json) { if (json != null) { - if (json.ToString().StartsWith("{\"ShowYourself\":")) + IJsonValue whatToShowJson; + if (json.TryGetValue("ShowYourself", out whatToShowJson)) { - if (json.ToString().EndsWith("\"flyout\"}")) + if (whatToShowJson.ValueType == JsonValueType.String && whatToShowJson.GetString().Equals("flyout")) { - OpenFlyoutCallback(); + POINT? p = null; + + IJsonValue flyoutPointX; + IJsonValue flyoutPointY; + if (json.TryGetValue("x_position", out flyoutPointX) && json.TryGetValue("y_position", out flyoutPointY)) + { + if (flyoutPointX.ValueType == JsonValueType.Number && flyoutPointY.ValueType == JsonValueType.Number) + { + int flyout_x = (int)flyoutPointX.GetNumber(); + int flyout_y = (int)flyoutPointY.GetNumber(); + p = new POINT(flyout_x, flyout_y); + } + } + + OpenFlyoutCallback(p); } - else if (json.ToString().EndsWith("\"main_page\"}")) + else if (whatToShowJson.ValueType == JsonValueType.String && whatToShowJson.GetString().Equals("main_page")) { OpenMainWindowCallback(); }