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 }