diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp
index aebf044f21..46ce729219 100644
--- a/src/common/interop/interop.cpp
+++ b/src/common/interop/interop.cpp
@@ -203,6 +203,10 @@ public
return gcnew String(CommonSharedConstants::SHOW_POWEROCR_SHARED_EVENT);
}
+ static String ^ MouseJumpShowPreviewEvent() {
+ return gcnew String(CommonSharedConstants::MOUSE_JUMP_SHOW_PREVIEW_EVENT);
+ }
+
static String ^ AwakeExitEvent() {
return gcnew String(CommonSharedConstants::AWAKE_EXIT_EVENT);
}
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index 1535a3aa87..4761d3c22c 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -50,6 +50,9 @@ namespace CommonSharedConstants
// Path to the event used by PowerOCR
const wchar_t SHOW_POWEROCR_SHARED_EVENT[] = L"Local\\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a";
+ // Path to the events used by Mouse Jump
+ const wchar_t MOUSE_JUMP_SHOW_PREVIEW_EVENT[] = L"Local\\MouseJumpEvent-aa0be051-3396-4976-b7ba-1a9cc7d236a5";
+
// Path to the event used by RegistryPreview
const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687";
diff --git a/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj.filters b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj.filters
new file mode 100644
index 0000000000..1d00e4c5b3
--- /dev/null
+++ b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj.filters
@@ -0,0 +1,56 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+ {875a08c6-f610-4667-bd0f-80171ed96072}
+
+
+
+
+ Header Files
+
+
+ Generated Files
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Resource Files
+
+
+
+
+
+ Resource Files
+
+
+
\ No newline at end of file
diff --git a/src/modules/MouseUtils/MouseJump/dllmain.cpp b/src/modules/MouseUtils/MouseJump/dllmain.cpp
index 8699fc4fad..d687caa063 100644
--- a/src/modules/MouseUtils/MouseJump/dllmain.cpp
+++ b/src/modules/MouseUtils/MouseJump/dllmain.cpp
@@ -1,32 +1,32 @@
+// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include
-//#include
-//#include
-#include
#include "trace.h"
-#include
+#include
+#include
+#include
#include
+#include
-extern "C" IMAGE_DOS_HEADER __ImageBase;
-
-HMODULE m_hModule;
-
-BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
+BOOL APIENTRY DllMain(HMODULE /*hModule*/,
+ DWORD ul_reason_for_call,
+ LPVOID /*lpReserved*/)
{
- m_hModule = hModule;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
+ break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
+
return TRUE;
}
@@ -54,9 +54,17 @@ private:
bool m_enabled = false;
// Hotkey to invoke the module
- Hotkey m_hotkey;
+
HANDLE m_hProcess;
+ // Time to wait for process to close after sending WM_CLOSE signal
+ static const int MAX_WAIT_MILLISEC = 10000;
+
+ Hotkey m_hotkey;
+
+ // Handle to event used to invoke PowerOCR
+ HANDLE m_hInvokeEvent;
+
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
@@ -123,11 +131,21 @@ private:
}
// Load initial settings from the persisted values.
- void init_settings();
-
- void terminate_process()
+ void init_settings()
{
- TerminateProcess(m_hProcess, 1);
+ try
+ {
+ // Load and parse the settings file for this PowerToy.
+ PowerToysSettings::PowerToyValues settings =
+ PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
+
+ parse_hotkey(settings);
+ }
+ catch (std::exception&)
+ {
+ Logger::warn(L"An exception occurred while loading the settings file");
+ // Error while loading from the settings file. Let default values stay as they are.
+ }
}
public:
@@ -135,6 +153,7 @@ public:
MouseJump()
{
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mouseJumpLoggerName);
+ m_hInvokeEvent = CreateDefaultEvent(CommonSharedConstants::MOUSE_JUMP_SHOW_PREVIEW_EVENT);
init_settings();
};
@@ -142,7 +161,6 @@ public:
{
if (m_enabled)
{
- terminate_process();
}
m_enabled = false;
}
@@ -150,6 +168,7 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
+ Logger::trace("MouseJump::destroy()");
delete this;
}
@@ -180,12 +199,14 @@ public:
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
+ settings.set_overview_link(L"https://aka.ms/PowerToysOverview_MouseUtilities/#mouse-jump");
+
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors.
- virtual void call_custom_action(const wchar_t* action) override
+ virtual void call_custom_action(const wchar_t* /*action*/) override
{
}
@@ -199,7 +220,6 @@ public:
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_hotkey(values);
-
values.save_to_settings_file();
}
catch (std::exception&)
@@ -211,6 +231,9 @@ public:
// Enable the powertoy
virtual void enable()
{
+ Logger::trace("MouseJump::enable()");
+ ResetEvent(m_hInvokeEvent);
+ launch_process();
m_enabled = true;
Trace::EnableJumpTool(true);
}
@@ -218,33 +241,29 @@ public:
// Disable the powertoy
virtual void disable()
{
+ Logger::trace("MouseJump::disable()");
if (m_enabled)
{
- terminate_process();
+ ResetEvent(m_hInvokeEvent);
+ TerminateProcess(m_hProcess, 1);
}
m_enabled = false;
Trace::EnableJumpTool(false);
}
- // Returns if the powertoys is enabled
- virtual bool is_enabled() override
- {
- return m_enabled;
- }
-
virtual bool on_hotkey(size_t /*hotkeyId*/) override
{
if (m_enabled)
{
Logger::trace(L"MouseJump hotkey pressed");
Trace::InvokeJumpTool();
- if (is_process_running())
+ if (!is_process_running())
{
- terminate_process();
+ launch_process();
}
- launch_process();
+ SetEvent(m_hInvokeEvent);
return true;
}
@@ -268,6 +287,12 @@ public:
}
}
+ // Returns if the powertoys is enabled
+ virtual bool is_enabled() override
+ {
+ return m_enabled;
+ }
+
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
@@ -276,26 +301,6 @@ public:
};
-// Load the settings file.
-void MouseJump::init_settings()
-{
- try
- {
- // Load and parse the settings file for this PowerToy.
- PowerToysSettings::PowerToyValues settings =
- PowerToysSettings::PowerToyValues::load_from_settings_file(MouseJump::get_name());
-
- parse_hotkey(settings);
-
- }
- catch (std::exception&)
- {
- Logger::warn(L"An exception occurred while loading the settings file");
- // Error while loading from the settings file. Let default values stay as they are.
- }
-}
-
-
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new MouseJump();
diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs
new file mode 100644
index 0000000000..c2602ad700
--- /dev/null
+++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs
@@ -0,0 +1,95 @@
+// 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.IO;
+using System.IO.Abstractions;
+using System.Threading;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Utilities;
+
+namespace MouseJumpUI.Helpers;
+
+internal class SettingsHelper
+{
+ public SettingsHelper()
+ {
+ this.LockObject = new();
+ this.CurrentSettings = this.LoadSettings();
+
+ // delay loading settings on change by some time to avoid file in use exception
+ var throttledActionInvoker = new ThrottledActionInvoker();
+ this.FileSystemWatcher = Helper.GetFileWatcher(
+ moduleName: MouseJumpSettings.ModuleName,
+ fileName: "settings.json",
+ onChangedCallback: () => throttledActionInvoker.ScheduleAction(this.ReloadSettings, 250));
+ }
+
+ private IFileSystemWatcher FileSystemWatcher
+ {
+ get;
+ }
+
+ private object LockObject
+ {
+ get;
+ }
+
+ public MouseJumpSettings CurrentSettings
+ {
+ get;
+ private set;
+ }
+
+ private MouseJumpSettings LoadSettings()
+ {
+ lock (this.LockObject)
+ {
+ {
+ var settingsUtils = new SettingsUtils();
+
+ // set this to 1 to disable retries
+ var remainingRetries = 5;
+
+ while (remainingRetries > 0)
+ {
+ try
+ {
+ if (!settingsUtils.SettingsExists(MouseJumpSettings.ModuleName))
+ {
+ Logger.LogInfo("MouseJump settings.json was missing, creating a new one");
+ var defaultSettings = new MouseJumpSettings();
+ defaultSettings.Save(settingsUtils);
+ }
+
+ var settings = settingsUtils.GetSettingsOrDefault(MouseJumpSettings.ModuleName);
+ return settings;
+ }
+ catch (IOException ex)
+ {
+ Logger.LogError("Failed to read changed settings", ex);
+ Thread.Sleep(250);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to read changed settings", ex);
+ Thread.Sleep(250);
+ }
+
+ remainingRetries--;
+ }
+ }
+ }
+
+ const string message = "Failed to read changed settings - ran out of retries";
+ Logger.LogError(message);
+ throw new InvalidOperationException(message);
+ }
+
+ public void ReloadSettings()
+ {
+ this.CurrentSettings = this.LoadSettings();
+ }
+}
diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs
new file mode 100644
index 0000000000..ecd17cfa4c
--- /dev/null
+++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs
@@ -0,0 +1,47 @@
+// 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.Windows.Threading;
+
+namespace MouseJumpUI.Helpers;
+
+internal sealed class ThrottledActionInvoker
+{
+ private readonly object _invokerLock = new();
+ private readonly DispatcherTimer _timer;
+
+ private Action? _actionToRun;
+
+ public ThrottledActionInvoker()
+ {
+ _timer = new DispatcherTimer();
+ _timer.Tick += Timer_Tick;
+ }
+
+ public void ScheduleAction(Action action, int milliseconds)
+ {
+ lock (_invokerLock)
+ {
+ if (_timer.IsEnabled)
+ {
+ _timer.Stop();
+ }
+
+ _actionToRun = action;
+ _timer.Interval = new TimeSpan(0, 0, 0, 0, milliseconds);
+
+ _timer.Start();
+ }
+ }
+
+ private void Timer_Tick(object? sender, EventArgs? e)
+ {
+ lock (_invokerLock)
+ {
+ _timer.Stop();
+ _actionToRun?.Invoke();
+ }
+ }
+}
diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs
index 7dbc1c6dd4..2b055aa862 100644
--- a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs
+++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs
@@ -19,14 +19,13 @@ namespace MouseJumpUI;
internal partial class MainForm : Form
{
- public MainForm(MouseJumpSettings settings)
+ public MainForm(SettingsHelper settingsHelper)
{
this.InitializeComponent();
- this.Settings = settings ?? throw new ArgumentNullException(nameof(settings));
- this.ShowThumbnail();
+ this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper));
}
- public MouseJumpSettings Settings
+ public SettingsHelper SettingsHelper
{
get;
}
@@ -104,14 +103,8 @@ internal partial class MainForm : Form
private void MainForm_Deactivate(object sender, EventArgs e)
{
- this.Close();
-
- if (this.Thumbnail.Image is not null)
- {
- var tmp = this.Thumbnail.Image;
- this.Thumbnail.Image = null;
- tmp.Dispose();
- }
+ this.Hide();
+ this.ClearPreview();
}
private void Thumbnail_Click(object sender, EventArgs e)
@@ -139,8 +132,11 @@ internal partial class MainForm : Form
this.OnDeactivate(EventArgs.Empty);
}
- public void ShowThumbnail()
+ public void ShowPreview()
{
+ // hide the form while we redraw it...
+ this.Visible = false;
+
var stopwatch = Stopwatch.StartNew();
var layoutInfo = MainForm.GetLayoutInfo(this);
LayoutHelper.PositionForm(this, layoutInfo.FormBounds);
@@ -148,7 +144,7 @@ internal partial class MainForm : Form
stopwatch.Stop();
// we have to activate the form to make sure the deactivate event fires
- Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
+ Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent());
this.Activate();
}
@@ -175,6 +171,9 @@ internal partial class MainForm : Form
.Single(item => item.Screen.Handle == activatedScreenHandle.Value)
.Index;
+ // avoid a race condition - cache the current settings in case they change
+ var currentSettings = form.SettingsHelper.CurrentSettings;
+
var layoutConfig = new LayoutConfig(
virtualScreenBounds: ScreenHelper.GetVirtualScreen(),
screens: screens.Select(item => item.Screen).ToList(),
@@ -182,13 +181,13 @@ internal partial class MainForm : Form
activatedScreenIndex: activatedScreenIndex,
activatedScreenNumber: activatedScreenIndex + 1,
maximumFormSize: new(
- form.Settings.Properties.ThumbnailSize.Width,
- form.Settings.Properties.ThumbnailSize.Height),
- formPadding: new(
- form.panel1.Padding.Left,
- form.panel1.Padding.Top,
- form.panel1.Padding.Right,
- form.panel1.Padding.Bottom),
+ currentSettings.Properties.ThumbnailSize.Width,
+ currentSettings.Properties.ThumbnailSize.Height),
+ /*
+ don't read the panel padding values because they are affected by dpi scaling
+ and can give wrong values when moving between monitors with different dpi scaling
+ */
+ formPadding: new(5, 5, 5, 5),
previewPadding: new(0));
Logger.LogInfo(string.Join(
'\n',
@@ -295,10 +294,30 @@ internal partial class MainForm : Form
stopwatch.Stop();
}
+ private void ClearPreview()
+ {
+ if (this.Thumbnail.Image is null)
+ {
+ return;
+ }
+
+ var tmp = this.Thumbnail.Image;
+ this.Thumbnail.Image = null;
+ tmp.Dispose();
+
+ // force preview image memory to be released, otherwise
+ // all the disposed images can pile up without being GC'ed
+ GC.Collect();
+ }
+
private static void RefreshPreview(MainForm form)
{
if (!form.Visible)
{
+ // we seem to need to turn off topmost and then re-enable it again
+ // when we show the form, otherwise it doesn't get shown topmost...
+ form.TopMost = false;
+ form.TopMost = true;
form.Show();
}
diff --git a/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj b/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj
index 478e7afe1e..d8cbe38df9 100644
--- a/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj
+++ b/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj
@@ -62,6 +62,7 @@
+
diff --git a/src/modules/MouseUtils/MouseJumpUI/Program.cs b/src/modules/MouseUtils/MouseJumpUI/Program.cs
index 7d3c385730..d70ebfa454 100644
--- a/src/modules/MouseUtils/MouseJumpUI/Program.cs
+++ b/src/modules/MouseUtils/MouseJumpUI/Program.cs
@@ -4,10 +4,16 @@
using System;
using System.IO;
+using System.Reflection;
using System.Text.Json;
+using System.Threading;
using System.Windows.Forms;
+using System.Windows.Threading;
+using Common.UI;
+using interop;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
+using MouseJumpUI.Helpers;
namespace MouseJumpUI;
@@ -17,7 +23,7 @@ internal static class Program
/// The main entry point for the application.
///
[STAThread]
- private static void Main()
+ private static void Main(string[] args)
{
Logger.InitializeLogger("\\MouseJump\\Logs");
@@ -38,10 +44,42 @@ internal static class Program
return;
}
- var settings = Program.ReadSettings();
- var mainForm = new MainForm(settings);
+ // validate command line arguments - we're expecting
+ // a single argument containing the runner pid
+ if ((args.Length != 1) || !int.TryParse(args[0], out var runnerPid))
+ {
+ var message = string.Join("\r\n", new[]
+ {
+ "Invalid command line arguments.",
+ "Expected usage is:",
+ string.Empty,
+ $"{Assembly.GetExecutingAssembly().GetName().Name} ",
+ });
+ Logger.LogInfo(message);
+ throw new InvalidOperationException(message);
+ }
- Application.Run(mainForm);
+ Logger.LogInfo($"Mouse Jump started from the PowerToys Runner. Runner pid={runnerPid}");
+
+ var cancellationTokenSource = new CancellationTokenSource();
+
+ RunnerHelper.WaitForPowerToysRunner(runnerPid, () =>
+ {
+ Logger.LogInfo("PowerToys Runner exited. Exiting Mouse Jump");
+ cancellationTokenSource.Cancel();
+ Application.Exit();
+ });
+
+ var settingsHelper = new SettingsHelper();
+ var mainForm = new MainForm(settingsHelper);
+
+ NativeEventWaiter.WaitForEventLoop(
+ Constants.MouseJumpShowPreviewEvent(),
+ mainForm.ShowPreview,
+ Dispatcher.CurrentDispatcher,
+ cancellationTokenSource.Token);
+
+ Application.Run();
}
private static MouseJumpSettings ReadSettings()
diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs
index 73b15f5086..2c0cf9eb3b 100644
--- a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs
@@ -2,6 +2,8 @@
// 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.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -21,6 +23,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Version = "1.0";
}
+ public void Save(ISettingsUtils settingsUtils)
+ {
+ // Save settings to file
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ };
+
+ if (settingsUtils == null)
+ {
+ throw new ArgumentNullException(nameof(settingsUtils));
+ }
+
+ settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName);
+ }
+
public string GetModuleName()
{
return Name;