OCROverlay.xaml.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.Collections.Generic; 7 using System.Globalization; 8 using System.Linq; 9 using System.Runtime.InteropServices; 10 using System.Windows; 11 using System.Windows.Controls; 12 using System.Windows.Controls.Primitives; 13 using System.Windows.Input; 14 using System.Windows.Interop; 15 using System.Windows.Media; 16 17 using Common.UI; 18 using ManagedCommon; 19 using Microsoft.PowerToys.Telemetry; 20 using PowerOCR.Helpers; 21 using PowerOCR.Settings; 22 using PowerOCR.Utilities; 23 using Windows.Globalization; 24 using Windows.Media.Ocr; 25 26 namespace PowerOCR; 27 28 /// <summary> 29 /// Interaction logic for MainWindow.xaml 30 /// </summary> 31 public partial class OCROverlay : Window 32 { 33 private bool isShiftDown; 34 private Point clickedPoint; 35 private Point shiftPoint; 36 private Border selectBorder = new(); 37 private Language? selectedLanguage; 38 39 private bool IsSelecting { get; set; } 40 41 private double selectLeft; 42 private double selectTop; 43 44 private double xShiftDelta; 45 private double yShiftDelta; 46 private bool isComboBoxReady; 47 private const double ActiveOpacity = 0.4; 48 private readonly UserSettings userSettings = new(new ThrottledActionInvoker()); 49 private System.Drawing.Rectangle screenRectangle; 50 private DpiScale dpiScale; 51 52 [DllImport("user32.dll", SetLastError = true)] 53 private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int nWidth, int nHeight, bool bRepaint); 54 55 public OCROverlay(System.Drawing.Rectangle screenRectangleParam, DpiScale dpiScaleParam) 56 { 57 screenRectangle = screenRectangleParam; 58 dpiScale = dpiScaleParam; 59 60 Left = screenRectangle.Left; 61 Top = screenRectangle.Top; 62 Width = screenRectangle.Width / dpiScale.DpiScaleX; 63 Height = screenRectangle.Height / dpiScale.DpiScaleY; 64 65 InitializeComponent(); 66 67 Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this, Wpf.Ui.Controls.WindowBackdropType.None); 68 69 PopulateLanguageMenu(); 70 } 71 72 private void PopulateLanguageMenu() 73 { 74 string? selectedLanguageName = userSettings.PreferredLanguage.Value; 75 76 // build context menu 77 if (string.IsNullOrEmpty(selectedLanguageName)) 78 { 79 selectedLanguage = ImageMethods.GetOCRLanguage(); 80 selectedLanguageName = selectedLanguage?.NativeName; 81 } 82 83 List<Language> possibleOcrLanguages = OcrEngine.AvailableRecognizerLanguages.ToList(); 84 85 int count = 0; 86 87 foreach (Language language in possibleOcrLanguages) 88 { 89 MenuItem menuItem = new() { Header = EnsureStartUpper(language.NativeName), Tag = language, IsCheckable = true }; 90 menuItem.IsChecked = language.NativeName.Equals(selectedLanguageName, StringComparison.OrdinalIgnoreCase); 91 LanguagesComboBox.Items.Add(new ComboBoxItem { Content = EnsureStartUpper(language.NativeName), Tag = language }); 92 if (language.NativeName.Equals(selectedLanguageName, StringComparison.OrdinalIgnoreCase)) 93 { 94 selectedLanguage = language; 95 LanguagesComboBox.SelectedIndex = count; 96 } 97 98 menuItem.Click += LanguageMenuItem_Click; 99 CanvasContextMenu.Items.Add(menuItem); 100 count++; 101 } 102 103 isComboBoxReady = true; 104 } 105 106 private void LanguageMenuItem_Click(object sender, RoutedEventArgs e) 107 { 108 MenuItem menuItem = (MenuItem)sender; 109 foreach (var item in CanvasContextMenu.Items) 110 { 111 if (item is MenuItem menuItemLoop) 112 { 113 menuItemLoop.IsChecked = item.Equals(menuItem); 114 } 115 } 116 117 selectedLanguage = menuItem.Tag as Language; 118 LanguagesComboBox.SelectedItem = selectedLanguage; 119 } 120 121 private void Window_Loaded(object sender, RoutedEventArgs e) 122 { 123 FullWindow.Rect = new Rect(0, 0, Width, Height); 124 KeyDown += MainWindow_KeyDown; 125 KeyUp += MainWindow_KeyUp; 126 127 BackgroundImage.Source = ImageMethods.GetWindowBoundsImage(this); 128 BackgroundBrush.Opacity = ActiveOpacity; 129 130 TopButtonsStackPanel.Visibility = Visibility.Visible; 131 132 #if DEBUG 133 Topmost = false; 134 #endif 135 IntPtr hwnd = new WindowInteropHelper(this).Handle; 136 137 // The first move puts it on the correct monitor, which triggers WM_DPICHANGED 138 // The +1/-1 coerces WPF to update Window.Top/Left/Width/Height in the second move 139 MoveWindow(hwnd, (int)(screenRectangle.Left + 1), (int)screenRectangle.Top, (int)(screenRectangle.Width - 1), (int)screenRectangle.Height, false); 140 MoveWindow(hwnd, (int)screenRectangle.Left, (int)screenRectangle.Top, (int)screenRectangle.Width, (int)screenRectangle.Height, true); 141 } 142 143 private void Window_Unloaded(object sender, RoutedEventArgs e) 144 { 145 BackgroundImage.Source = null; 146 BackgroundImage.UpdateLayout(); 147 148 KeyDown -= MainWindow_KeyDown; 149 KeyUp -= MainWindow_KeyUp; 150 151 Loaded -= Window_Loaded; 152 Unloaded -= Window_Unloaded; 153 154 RegionClickCanvas.MouseDown -= RegionClickCanvas_MouseDown; 155 RegionClickCanvas.MouseUp -= RegionClickCanvas_MouseUp; 156 RegionClickCanvas.MouseMove -= RegionClickCanvas_MouseMove; 157 } 158 159 private void MainWindow_KeyUp(object sender, KeyEventArgs e) 160 { 161 switch (e.Key) 162 { 163 case Key.LeftShift: 164 case Key.RightShift: 165 isShiftDown = false; 166 clickedPoint = new Point(clickedPoint.X + xShiftDelta, clickedPoint.Y + yShiftDelta); 167 break; 168 default: 169 break; 170 } 171 } 172 173 private void MainWindow_KeyDown(object sender, KeyEventArgs e) 174 { 175 WindowUtilities.OcrOverlayKeyDown(e.Key); 176 } 177 178 private void RegionClickCanvas_MouseDown(object sender, MouseButtonEventArgs e) 179 { 180 if (e.LeftButton != MouseButtonState.Pressed) 181 { 182 return; 183 } 184 185 TopButtonsStackPanel.Visibility = Visibility.Collapsed; 186 RegionClickCanvas.CaptureMouse(); 187 188 CursorClipper.ClipCursor(this); 189 clickedPoint = e.GetPosition(this); 190 selectBorder.Height = 1; 191 selectBorder.Width = 1; 192 193 try 194 { 195 RegionClickCanvas.Children.Remove(selectBorder); 196 } 197 catch (Exception) 198 { 199 } 200 201 selectBorder.BorderThickness = new Thickness(2); 202 Color borderColor = Color.FromArgb(255, 40, 118, 126); 203 selectBorder.BorderBrush = new SolidColorBrush(borderColor); 204 _ = RegionClickCanvas.Children.Add(selectBorder); 205 Canvas.SetLeft(selectBorder, clickedPoint.X); 206 Canvas.SetTop(selectBorder, clickedPoint.Y); 207 208 IsSelecting = true; 209 } 210 211 private void RegionClickCanvas_MouseMove(object sender, MouseEventArgs e) 212 { 213 if (!IsSelecting) 214 { 215 return; 216 } 217 218 Point movingPoint = e.GetPosition(this); 219 220 if (System.Windows.Input.Keyboard.Modifiers == ModifierKeys.Shift) 221 { 222 if (!isShiftDown) 223 { 224 shiftPoint = movingPoint; 225 selectLeft = Canvas.GetLeft(selectBorder); 226 selectTop = Canvas.GetTop(selectBorder); 227 } 228 229 isShiftDown = true; 230 xShiftDelta = movingPoint.X - shiftPoint.X; 231 yShiftDelta = movingPoint.Y - shiftPoint.Y; 232 233 double leftValue = selectLeft + xShiftDelta; 234 double topValue = selectTop + yShiftDelta; 235 236 clippingGeometry.Rect = new Rect( 237 new Point(leftValue, topValue), 238 new Size(selectBorder.Width, selectBorder.Height)); 239 Canvas.SetLeft(selectBorder, leftValue - 1); 240 Canvas.SetTop(selectBorder, topValue - 1); 241 return; 242 } 243 244 isShiftDown = false; 245 246 double left = Math.Min(clickedPoint.X, movingPoint.X); 247 double top = Math.Min(clickedPoint.Y, movingPoint.Y); 248 249 selectBorder.Height = Math.Max(clickedPoint.Y, movingPoint.Y) - top; 250 selectBorder.Width = Math.Max(clickedPoint.X, movingPoint.X) - left; 251 selectBorder.Height += 2; 252 selectBorder.Width += 2; 253 254 clippingGeometry.Rect = new Rect( 255 new Point(left, top), 256 new Size(selectBorder.Width - 2, selectBorder.Height - 2)); 257 Canvas.SetLeft(selectBorder, left - 1); 258 Canvas.SetTop(selectBorder, top - 1); 259 } 260 261 private async void RegionClickCanvas_MouseUp(object sender, MouseButtonEventArgs e) 262 { 263 if (IsSelecting == false) 264 { 265 return; 266 } 267 268 TopButtonsStackPanel.Visibility = Visibility.Visible; 269 IsSelecting = false; 270 271 CursorClipper.UnClipCursor(); 272 RegionClickCanvas.ReleaseMouseCapture(); 273 Matrix m = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice; 274 275 Point movingPoint = e.GetPosition(this); 276 movingPoint.X *= m.M11; 277 movingPoint.Y *= m.M22; 278 279 movingPoint.X = Math.Round(movingPoint.X); 280 movingPoint.Y = Math.Round(movingPoint.Y); 281 282 double xDimScaled = Canvas.GetLeft(selectBorder) * m.M11; 283 double yDimScaled = Canvas.GetTop(selectBorder) * m.M22; 284 285 System.Drawing.Rectangle regionScaled = new( 286 (int)xDimScaled, 287 (int)yDimScaled, 288 (int)(selectBorder.Width * m.M11), 289 (int)(selectBorder.Height * m.M22)); 290 291 string grabbedText; 292 293 try 294 { 295 RegionClickCanvas.Children.Remove(selectBorder); 296 clippingGeometry.Rect = new Rect(0, 0, 0, 0); 297 } 298 catch 299 { 300 } 301 302 if (regionScaled.Width < 3 || regionScaled.Height < 3) 303 { 304 BackgroundBrush.Opacity = 0; 305 Logger.LogInfo($"Getting clicked word, {selectedLanguage?.LanguageTag}"); 306 grabbedText = await ImageMethods.GetClickedWord(this, new Point(xDimScaled, yDimScaled), selectedLanguage); 307 } 308 else 309 { 310 if (TableMenuItem.IsChecked) 311 { 312 Logger.LogInfo($"Getting region as table, {selectedLanguage?.LanguageTag}"); 313 grabbedText = await OcrExtensions.GetRegionsTextAsTableAsync(this, regionScaled, selectedLanguage); 314 } 315 else 316 { 317 Logger.LogInfo($"Standard region capture, {selectedLanguage?.LanguageTag}"); 318 grabbedText = await ImageMethods.GetRegionsText(this, regionScaled, selectedLanguage); 319 320 if (SingleLineMenuItem.IsChecked) 321 { 322 Logger.LogInfo($"Making grabbed text single line"); 323 grabbedText = grabbedText.MakeStringSingleLine(); 324 } 325 } 326 } 327 328 if (string.IsNullOrWhiteSpace(grabbedText)) 329 { 330 BackgroundBrush.Opacity = ActiveOpacity; 331 return; 332 } 333 334 try 335 { 336 Clipboard.SetText(grabbedText); 337 } 338 catch (Exception ex) 339 { 340 Logger.LogError($"Clipboard.SetText exception: {ex}"); 341 } 342 343 WindowUtilities.CloseAllOCROverlays(); 344 PowerToysTelemetry.Log.WriteEvent(new PowerOCR.Telemetry.PowerOCRCaptureEvent()); 345 } 346 347 private void CancelMenuItem_Click(object sender, RoutedEventArgs e) 348 { 349 WindowUtilities.CloseAllOCROverlays(); 350 PowerToysTelemetry.Log.WriteEvent(new PowerOCR.Telemetry.PowerOCRCancelledEvent()); 351 } 352 353 private void LanguagesComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 354 { 355 if (sender is not ComboBox languageComboBox || !isComboBoxReady) 356 { 357 return; 358 } 359 360 // TODO: Set the preferred language based upon what was chosen here 361 int selection = languageComboBox.SelectedIndex; 362 selectedLanguage = (languageComboBox.SelectedItem as ComboBoxItem)?.Tag as Language; 363 364 if (selectedLanguage == null) 365 { 366 return; 367 } 368 369 Logger.LogError($"Changed language to {selectedLanguage?.LanguageTag}"); 370 371 // Set the language in the context menu 372 foreach (var item in CanvasContextMenu.Items) 373 { 374 if (item is MenuItem menuItemLoop) 375 { 376 menuItemLoop.IsChecked = menuItemLoop.Tag as Language == selectedLanguage; 377 } 378 } 379 380 switch (selection) 381 { 382 case 0: 383 WindowUtilities.OcrOverlayKeyDown(Key.D1); 384 break; 385 case 1: 386 WindowUtilities.OcrOverlayKeyDown(Key.D2); 387 break; 388 case 2: 389 WindowUtilities.OcrOverlayKeyDown(Key.D3); 390 break; 391 case 3: 392 WindowUtilities.OcrOverlayKeyDown(Key.D4); 393 break; 394 case 4: 395 WindowUtilities.OcrOverlayKeyDown(Key.D5); 396 break; 397 case 5: 398 WindowUtilities.OcrOverlayKeyDown(Key.D6); 399 break; 400 case 6: 401 WindowUtilities.OcrOverlayKeyDown(Key.D7); 402 break; 403 case 7: 404 WindowUtilities.OcrOverlayKeyDown(Key.D8); 405 break; 406 case 8: 407 WindowUtilities.OcrOverlayKeyDown(Key.D9); 408 break; 409 default: 410 break; 411 } 412 } 413 414 private void SingleLineMenuItem_Click(object sender, RoutedEventArgs e) 415 { 416 bool isActive = CheckIfCheckingOrUnchecking(sender); 417 WindowUtilities.OcrOverlayKeyDown(Key.S, isActive); 418 } 419 420 private void TableToggleButton_Click(object sender, RoutedEventArgs e) 421 { 422 bool isActive = CheckIfCheckingOrUnchecking(sender); 423 WindowUtilities.OcrOverlayKeyDown(Key.T, isActive); 424 } 425 426 private void SettingsMenuItem_Click(object sender, RoutedEventArgs e) 427 { 428 WindowUtilities.CloseAllOCROverlays(); 429 SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerOCR); 430 } 431 432 private static bool CheckIfCheckingOrUnchecking(object? sender) 433 { 434 if (sender is ToggleButton tb && tb.IsChecked is not null) 435 { 436 return tb.IsChecked.Value; 437 } 438 439 if (sender is MenuItem mi) 440 { 441 return mi.IsChecked; 442 } 443 444 return false; 445 } 446 447 internal void KeyPressed(Key key, bool? isActive) 448 { 449 switch (key) 450 { 451 // This case is handled in the WindowUtilities.OcrOverlayKeyDown 452 // case Key.Escape: 453 // WindowUtilities.CloseAllFullscreenGrabs(); 454 // break; 455 case Key.S: 456 if (isActive is null) 457 { 458 SingleLineMenuItem.IsChecked = !SingleLineMenuItem.IsChecked; 459 } 460 else 461 { 462 SingleLineMenuItem.IsChecked = isActive.Value; 463 } 464 465 // Possibly save this in settings later and remember this preference 466 break; 467 case Key.T: 468 if (isActive is null) 469 { 470 TableToggleButton.IsChecked = !TableToggleButton.IsChecked; 471 } 472 else 473 { 474 TableToggleButton.IsChecked = isActive.Value; 475 } 476 477 break; 478 case Key.D1: 479 case Key.D2: 480 case Key.D3: 481 case Key.D4: 482 case Key.D5: 483 case Key.D6: 484 case Key.D7: 485 case Key.D8: 486 case Key.D9: 487 int numberPressed = (int)key - 34; // D1 casts to 35, D2 to 36, etc. 488 int numberOfLanguages = LanguagesComboBox.Items.Count; 489 490 if (numberPressed <= numberOfLanguages 491 && numberPressed - 1 >= 0 492 && numberPressed - 1 != LanguagesComboBox.SelectedIndex 493 && isComboBoxReady) 494 { 495 LanguagesComboBox.SelectedIndex = numberPressed - 1; 496 } 497 498 break; 499 default: 500 break; 501 } 502 } 503 504 public System.Drawing.Rectangle GetScreenRectangle() 505 { 506 return screenRectangle; 507 } 508 509 private string EnsureStartUpper(string input) 510 { 511 if (string.IsNullOrEmpty(input)) 512 { 513 return input; 514 } 515 516 var inputArray = input.ToCharArray(); 517 inputArray[0] = char.ToUpper(inputArray[0], CultureInfo.CurrentCulture); 518 return new string(inputArray); 519 } 520 }