/ src / modules / MouseUtils / MouseJumpUI / MainForm.cs
MainForm.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System;
  6  using System.Diagnostics;
  7  using System.Drawing;
  8  using System.Linq;
  9  using System.Windows.Forms;
 10  
 11  using ManagedCommon;
 12  using MouseJump.Common.Helpers;
 13  using MouseJump.Common.Imaging;
 14  using MouseJump.Common.Models.Drawing;
 15  using MouseJump.Common.Models.Layout;
 16  using MouseJumpUI.Helpers;
 17  
 18  namespace MouseJumpUI;
 19  
 20  internal sealed partial class MainForm : Form
 21  {
 22      public MainForm(SettingsHelper settingsHelper)
 23      {
 24          this.InitializeComponent();
 25          this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper));
 26      }
 27  
 28      private PreviewLayout? PreviewLayout
 29      {
 30          get;
 31          set;
 32      }
 33  
 34      public SettingsHelper SettingsHelper
 35      {
 36          get;
 37      }
 38  
 39      private void MainForm_Load(object sender, EventArgs e)
 40      {
 41      }
 42  
 43      private void MainForm_KeyDown(object sender, KeyEventArgs e)
 44      {
 45          if (e.KeyCode == Keys.Escape)
 46          {
 47              this.OnDeactivate(EventArgs.Empty);
 48              return;
 49          }
 50  
 51          var screens = ScreenHelper.GetAllScreens().ToList();
 52          if (screens.Count == 0)
 53          {
 54              return;
 55          }
 56  
 57          var currentLocation = MouseHelper.GetCursorPosition();
 58          var currentScreen = ScreenHelper.GetScreenFromPoint(screens, currentLocation);
 59          var currentScreenIndex = screens.IndexOf(currentScreen);
 60          var targetScreen = default(ScreenInfo?);
 61  
 62          switch (e.KeyCode)
 63          {
 64              case >= Keys.D1 and <= Keys.D9:
 65                  {
 66                      // number keys 1-9 - move to the numbered screen
 67                      var screenNumber = e.KeyCode - Keys.D0;
 68                      /* note - screen *numbers* are 1-based, screen *indexes* are 0-based */
 69                      targetScreen = (screenNumber <= screens.Count)
 70                          ? targetScreen = screens[screenNumber - 1]
 71                          : null;
 72                      break;
 73                  }
 74  
 75              case >= Keys.NumPad1 and <= Keys.NumPad9:
 76                  {
 77                      // numpad keys 1-9 - move to the numbered screen
 78                      var screenNumber = e.KeyCode - Keys.NumPad0;
 79                      /* note - screen *numbers* are 1-based, screen *indexes* are 0-based */
 80                      targetScreen = (screenNumber <= screens.Count)
 81                          ? targetScreen = screens[screenNumber - 1]
 82                          : null;
 83                      break;
 84                  }
 85  
 86              case Keys.P:
 87                  // "P" - move to the primary screen
 88                  targetScreen = screens.Single(screen => screen.Primary);
 89                  break;
 90              case Keys.Left:
 91                  // move to the previous screen, looping back to the end if needed
 92                  var prevIndex = (currentScreenIndex - 1 + screens.Count) % screens.Count;
 93                  targetScreen = screens[prevIndex];
 94                  break;
 95              case Keys.Right:
 96                  // move to the next screen, looping round to the start if needed
 97                  var nextIndex = (currentScreenIndex + 1) % screens.Count;
 98                  targetScreen = screens[nextIndex];
 99                  break;
100              case Keys.Home:
101                  // move to the first screen
102                  targetScreen = screens.First();
103                  break;
104              case Keys.End:
105                  // move to the last screen
106                  targetScreen = screens.Last();
107                  break;
108          }
109  
110          if (targetScreen is not null)
111          {
112              MouseHelper.SetCursorPosition(targetScreen.DisplayArea.Midpoint);
113              this.OnDeactivate(EventArgs.Empty);
114          }
115      }
116  
117      private void MainForm_Deactivate(object sender, EventArgs e)
118      {
119          this.Hide();
120          this.ClearPreview();
121      }
122  
123      private void Thumbnail_Click(object sender, EventArgs e)
124      {
125          var mouseEventArgs = (MouseEventArgs)e;
126          Logger.LogInfo(string.Join(
127              '\n',
128              $"Reporting mouse event args",
129              $"\tbutton   = {mouseEventArgs.Button}",
130              $"\tlocation = {mouseEventArgs.Location}"));
131  
132          if (mouseEventArgs.Button == MouseButtons.Left)
133          {
134              if (this.PreviewLayout is null)
135              {
136                  // there's no layout data so we can't work out what screen was clicked
137                  return;
138              }
139  
140              // work out which screenshot was clicked
141              var clickedScreenshot = this.PreviewLayout.ScreenshotBounds
142                  .FirstOrDefault(
143                      box => box.BorderBounds.Contains(mouseEventArgs.X, mouseEventArgs.Y));
144              if (clickedScreenshot is null)
145              {
146                  return;
147              }
148  
149              // scale up the click onto the physical screen - the aspect ratio of the screenshot
150              // might be distorted compared to the physical screen due to the borders around the
151              // screenshot, so we need to work out the target location on the physical screen first
152              var clickedScreen =
153                  this.PreviewLayout.Screens[this.PreviewLayout.ScreenshotBounds.IndexOf(clickedScreenshot)];
154              var clickedLocation = new PointInfo(mouseEventArgs.Location)
155                  .Stretch(
156                      source: clickedScreenshot.ContentBounds,
157                      target: clickedScreen)
158                  .Clamp(
159                      new(
160                          x: clickedScreen.X + 1,
161                          y: clickedScreen.Y + 1,
162                          width: clickedScreen.Width - 1,
163                          height: clickedScreen.Height - 1
164                      ))
165                  .Truncate();
166  
167              // move mouse pointer
168              Logger.LogInfo($"clicked location = {clickedLocation}");
169              Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent());
170              MouseHelper.SetCursorPosition(clickedLocation);
171          }
172  
173          this.OnDeactivate(EventArgs.Empty);
174      }
175  
176      public void ShowPreview()
177      {
178          // hide the form while we redraw it...
179          this.Visible = false;
180  
181          var stopwatch = Stopwatch.StartNew();
182  
183          var appSettings = this.SettingsHelper.CurrentSettings ?? throw new InvalidOperationException();
184          var screens = ScreenHelper.GetAllScreens().Select(screen => screen.DisplayArea).ToList();
185          var activatedLocation = MouseHelper.GetCursorPosition();
186  
187          this.PreviewLayout = LayoutHelper.GetPreviewLayout(
188              previewStyle: SettingsHelper.GetActivePreviewStyle(appSettings),
189              screens: screens,
190              activatedLocation: activatedLocation);
191  
192          this.PositionForm(this.PreviewLayout.FormBounds);
193  
194          var imageCopyService = new DesktopImageRegionCopyService();
195          DrawingHelper.RenderPreview(
196              this.PreviewLayout,
197              imageCopyService,
198              this.OnPreviewImageCreated,
199              this.OnPreviewImageUpdated);
200  
201          stopwatch.Stop();
202  
203          Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent());
204  
205          // we have to activate the form to make sure the deactivate event fires
206          this.Activate();
207      }
208  
209      private void ClearPreview()
210      {
211          if (this.Thumbnail.Image is null)
212          {
213              return;
214          }
215  
216          var tmp = this.Thumbnail.Image;
217          this.Thumbnail.Image = null;
218          tmp.Dispose();
219  
220          // force preview image memory to be released; otherwise,
221          // all the disposed images can pile up without being GC'ed
222          GC.Collect();
223      }
224  
225      /// <summary>
226      /// Resize and position the specified form.
227      /// </summary>
228      private void PositionForm(RectangleInfo bounds)
229      {
230          // note - do this in two steps rather than "this.Bounds = formBounds" as there
231          // appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2,
232          // where the form scaling uses either the *primary* screen scaling or the *previous*
233          // screen's scaling when the form is moved to a different screen. i've got no idea
234          // *why*, but the exact sequence of calls below seems to be a workaround...
235          // see https://github.com/mikeclayton/FancyMouse/issues/2
236          var rect = bounds.ToRectangle();
237          this.Location = rect.Location;
238          _ = this.PointToScreen(Point.Empty);
239          this.Size = rect.Size;
240      }
241  
242      private void OnPreviewImageCreated(Bitmap preview)
243      {
244          this.ClearPreview();
245          this.Thumbnail.Image = preview;
246      }
247  
248      private void OnPreviewImageUpdated()
249      {
250          if (!this.Visible)
251          {
252              // we seem to need to turn off topmost and then re-enable it again
253              // when we show the form; otherwise, it doesn't always get shown topmost...
254              this.TopMost = false;
255              this.TopMost = true;
256              this.Show();
257          }
258  
259          this.Thumbnail.Refresh();
260      }
261  }