MainViewModel.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.Collections.ObjectModel; 8 using System.Globalization; 9 using System.Linq; 10 using System.Reflection; 11 using System.Runtime.InteropServices; 12 using System.Text; 13 using System.Threading; 14 using System.Threading.Tasks; 15 using System.Windows; 16 using System.Windows.Input; 17 using System.Windows.Interop; 18 using System.Windows.Threading; 19 20 using Common.UI; 21 using Mages.Core.Runtime.Converters; 22 using Microsoft.PowerLauncher.Telemetry; 23 using Microsoft.PowerToys.Telemetry; 24 using PowerLauncher.Helper; 25 using PowerLauncher.Plugin; 26 using PowerLauncher.Storage; 27 using PowerToys.Interop; 28 using Wox.Infrastructure; 29 using Wox.Infrastructure.Hotkey; 30 using Wox.Infrastructure.Storage; 31 using Wox.Infrastructure.UserSettings; 32 using Wox.Plugin; 33 using Wox.Plugin.Logger; 34 35 namespace PowerLauncher.ViewModel 36 { 37 [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1309:Use ordinal string comparison", Justification = "Using CurrentCultureIgnoreCase for user facing strings. Each usage is attributed with a comment.")] 38 public class MainViewModel : BaseModel, IMainViewModel, ISavable, IDisposable 39 { 40 private string _currentQuery; 41 private static string _emptyQuery = string.Empty; 42 43 private static bool _disposed; 44 45 private readonly WoxJsonStorage<QueryHistory> _historyItemsStorage; 46 private readonly WoxJsonStorage<UserSelectedRecord> _userSelectedRecordStorage; 47 private readonly PowerToysRunSettings _settings; 48 private readonly QueryHistory _history; 49 private readonly UserSelectedRecord _userSelectedRecord; 50 private static readonly Lock _addResultsLock = new Lock(); 51 private readonly System.Diagnostics.Stopwatch _hotkeyTimer = new System.Diagnostics.Stopwatch(); 52 53 private string _queryTextBeforeLeaveResults; 54 55 private CancellationTokenSource _updateSource; 56 57 private CancellationToken _updateToken; 58 private CancellationToken _nativeWaiterCancelToken; 59 private bool _saved; 60 private ushort _hotkeyHandle; 61 62 private const int _globalHotKeyId = 0x0001; 63 private IntPtr _globalHotKeyHwnd; 64 private uint _globalHotKeyVK; 65 private uint _globalHotKeyFSModifiers; 66 private bool _usingGlobalHotKey; 67 68 internal HotkeyManager HotkeyManager { get; private set; } 69 70 private static readonly CompositeFormat RegisterHotkeyFailed = System.Text.CompositeFormat.Parse(Properties.Resources.registerHotkeyFailed); 71 72 public MainViewModel(PowerToysRunSettings settings, CancellationToken nativeThreadCancelToken) 73 { 74 _saved = false; 75 _queryTextBeforeLeaveResults = string.Empty; 76 _currentQuery = _emptyQuery; 77 _disposed = false; 78 79 _settings = settings ?? throw new ArgumentNullException(nameof(settings)); 80 _nativeWaiterCancelToken = nativeThreadCancelToken; 81 _historyItemsStorage = new WoxJsonStorage<QueryHistory>(); 82 _userSelectedRecordStorage = new WoxJsonStorage<UserSelectedRecord>(); 83 _history = _historyItemsStorage.Load(); 84 _userSelectedRecord = _userSelectedRecordStorage.Load(); 85 86 ContextMenu = new ResultsViewModel(_settings, this); 87 Results = new ResultsViewModel(_settings, this); 88 History = new ResultsViewModel(_settings, this); 89 _selectedResults = Results; 90 InitializeKeyCommands(); 91 RegisterResultsUpdatedEvent(); 92 } 93 94 public void RemoveUserSelectedRecord(Result result) 95 { 96 _userSelectedRecord.Remove(result); 97 } 98 99 public void RegisterHotkey(IntPtr hwnd) 100 { 101 Log.Info("RegisterHotkey()", GetType()); 102 103 // Allow OOBE to call PowerToys Run. 104 NativeEventWaiter.WaitForEventLoop(Constants.PowerLauncherSharedEvent(), OnHotkey, Application.Current.Dispatcher, _nativeWaiterCancelToken); 105 106 if (_settings.StartedFromPowerToysRunner) 107 { 108 // Allow runner to call PowerToys Run from the centralized keyboard hook. 109 NativeEventWaiter.WaitForEventLoop(Constants.PowerLauncherCentralizedHookSharedEvent(), OnCentralizedKeyboardHookHotKey, Application.Current.Dispatcher, _nativeWaiterCancelToken); 110 } 111 112 _settings.PropertyChanged += (s, e) => 113 { 114 if (e.PropertyName == nameof(PowerToysRunSettings.Hotkey) || e.PropertyName == nameof(PowerToysRunSettings.UseCentralizedKeyboardHook)) 115 { 116 Application.Current.Dispatcher.Invoke(() => 117 { 118 if (!string.IsNullOrEmpty(_settings.PreviousHotkey)) 119 { 120 if (_usingGlobalHotKey) 121 { 122 NativeMethods.UnregisterHotKey(_globalHotKeyHwnd, _globalHotKeyId); 123 _usingGlobalHotKey = false; 124 Log.Info("Unregistering previous global hotkey", GetType()); 125 } 126 127 if (_hotkeyHandle != 0) 128 { 129 HotkeyManager?.UnregisterHotkey(_hotkeyHandle); 130 _hotkeyHandle = 0; 131 Log.Info("Unregistering previous low level key handler", GetType()); 132 } 133 } 134 135 if (!string.IsNullOrEmpty(_settings.Hotkey)) 136 { 137 SetHotkey(hwnd, _settings.Hotkey, OnHotkey); 138 } 139 }); 140 } 141 else if (e.PropertyName == nameof(PowerToysRunSettings.ShowPluginsOverview)) 142 { 143 RefreshPluginsOverview(); 144 } 145 }; 146 147 SetHotkey(hwnd, _settings.Hotkey, OnHotkey); 148 149 // TODO: Custom plugin hotkeys. 150 // SetCustomPluginHotkey(); 151 } 152 153 public void RegisterSettingsChangeListener(System.ComponentModel.PropertyChangedEventHandler handler) 154 { 155 _settings.PropertyChanged += handler; 156 } 157 158 private void RegisterResultsUpdatedEvent() 159 { 160 foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>()) 161 { 162 var plugin = (IResultUpdated)pair.Plugin; 163 plugin.ResultsUpdated += (s, e) => 164 { 165 Task.Run( 166 () => 167 { 168 PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); 169 UpdateResultView(e.Results, e.Query.RawQuery, _updateToken); 170 }, 171 _updateToken); 172 }; 173 } 174 } 175 176 private void OpenResultsEvent(object index, bool isMouseClick) 177 { 178 var results = SelectedResults; 179 180 if (index != null) 181 { 182 // Using InvariantCulture since this is internal 183 results.SelectedIndex = int.Parse(index.ToString(), CultureInfo.InvariantCulture); 184 } 185 186 if (results.SelectedItem != null) 187 { 188 bool executeResultRequired = false; 189 190 if (isMouseClick) 191 { 192 executeResultRequired = true; 193 } 194 else 195 { 196 // If there is a context button selected fire the action for that button instead, and the main command will not be executed 197 executeResultRequired = !results.SelectedItem.ExecuteSelectedContextButton(); 198 } 199 200 if (executeResultRequired) 201 { 202 var result = results.SelectedItem.Result; 203 204 // SelectedItem returns null if selection is empty. 205 if (result != null && result.Action != null) 206 { 207 bool hideWindow = true; 208 209 Application.Current.Dispatcher.Invoke(() => 210 { 211 hideWindow = result.Action(new ActionContext 212 { 213 SpecialKeyState = KeyboardHelper.CheckModifiers(), 214 }); 215 }); 216 217 if (hideWindow) 218 { 219 Hide(); 220 } 221 222 if (SelectedIsFromQueryResults()) 223 { 224 _userSelectedRecord.Add(result); 225 _history.Add(result.OriginQuery.RawQuery); 226 } 227 else 228 { 229 SelectedResults = Results; 230 } 231 } 232 } 233 } 234 } 235 236 private void InitializeKeyCommands() 237 { 238 IgnoreCommand = new RelayCommand(_ => { }); 239 240 EscCommand = new RelayCommand(_ => 241 { 242 if (!SelectedIsFromQueryResults()) 243 { 244 SelectedResults = Results; 245 } 246 else 247 { 248 Hide(); 249 } 250 }); 251 252 SelectNextItemCommand = new RelayCommand(_ => 253 { 254 SelectedResults.SelectNextResult(); 255 }); 256 257 SelectPrevItemCommand = new RelayCommand(_ => 258 { 259 SelectedResults.SelectPrevResult(); 260 }); 261 262 SelectNextTabItemCommand = new RelayCommand(_ => 263 { 264 SelectedResults.SelectNextTabItem(); 265 }); 266 267 SelectPrevTabItemCommand = new RelayCommand(_ => 268 { 269 SelectedResults.SelectPrevTabItem(); 270 }); 271 272 SelectNextContextMenuItemCommand = new RelayCommand(_ => 273 { 274 SelectedResults.SelectNextContextMenuItem(); 275 }); 276 277 SelectPreviousContextMenuItemCommand = new RelayCommand(_ => 278 { 279 SelectedResults.SelectPreviousContextMenuItem(); 280 }); 281 282 SelectNextPageCommand = new RelayCommand(_ => 283 { 284 SelectedResults.SelectNextPage(); 285 }); 286 287 SelectPrevPageCommand = new RelayCommand(_ => 288 { 289 SelectedResults.SelectPrevPage(); 290 }); 291 292 OpenResultWithKeyboardCommand = new RelayCommand(index => 293 { 294 OpenResultsEvent(index, false); 295 }); 296 297 OpenResultWithMouseCommand = new RelayCommand(index => 298 { 299 OpenResultsEvent(index, true); 300 }); 301 302 ClearQueryCommand = new RelayCommand(_ => 303 { 304 if (!string.IsNullOrEmpty(QueryText)) 305 { 306 ChangeQueryText(string.Empty, true); 307 308 // Push Event to UI SystemQuery has changed 309 OnPropertyChanged(nameof(SystemQueryText)); 310 } 311 }); 312 313 SelectNextOverviewPluginCommand = new RelayCommand(_ => 314 { 315 SelectNextOverviewPlugin(); 316 }); 317 318 SelectPrevOverviewPluginCommand = new RelayCommand(_ => 319 { 320 SelectPrevOverviewPlugin(); 321 }); 322 } 323 324 private ResultsViewModel _results; 325 326 public ResultsViewModel Results 327 { 328 get => _results; 329 private set 330 { 331 if (value != _results) 332 { 333 _results = value; 334 OnPropertyChanged(nameof(Results)); 335 } 336 } 337 } 338 339 public ResultsViewModel ContextMenu { get; private set; } 340 341 public ResultsViewModel History { get; private set; } 342 343 private string _systemQueryText = string.Empty; 344 345 public string SystemQueryText 346 { 347 get => _systemQueryText; 348 set 349 { 350 if (_systemQueryText != value) 351 { 352 _systemQueryText = value; 353 OnPropertyChanged(nameof(SystemQueryText)); 354 } 355 } 356 } 357 358 private string _queryText = string.Empty; 359 360 public string QueryText 361 { 362 get => _queryText; 363 364 set 365 { 366 if (_queryText != value) 367 { 368 _queryText = value; 369 370 SetPluginsOverviewVisibility(); 371 OnPropertyChanged(nameof(QueryText)); 372 } 373 } 374 } 375 376 /// <summary> 377 /// we need move cursor to end when we manually changed query 378 /// but we don't want to move cursor to end when query is updated from TextBox. 379 /// Also we don't want to force the results to change unless explicitly told to. 380 /// </summary> 381 /// <param name="queryText">Text that is being queried from user</param> 382 /// <param name="requery">Optional Parameter that if true, will automatically execute a query against the updated text</param> 383 public void ChangeQueryText(string queryText, bool requery = false) 384 { 385 var sameQueryText = SystemQueryText == queryText; 386 387 SystemQueryText = queryText; 388 389 if (sameQueryText) 390 { 391 OnPropertyChanged(nameof(SystemQueryText)); 392 } 393 394 if (requery) 395 { 396 QueryText = queryText; 397 Query(); 398 } 399 } 400 401 public void SetPluginsOverviewVisibility() 402 { 403 if (_settings.ShowPluginsOverview != PowerToysRunSettings.ShowPluginsOverviewMode.None && (string.IsNullOrEmpty(_queryText) || string.IsNullOrWhiteSpace(_queryText))) 404 { 405 PluginsOverviewVisibility = Visibility.Visible; 406 } 407 else 408 { 409 PluginsOverviewVisibility = Visibility.Collapsed; 410 } 411 } 412 413 public void SetFontSize() 414 { 415 App.Current.Resources["TitleFontSize"] = (double)_settings.TitleFontSize; 416 } 417 418 public bool LastQuerySelected { get; set; } 419 420 private ResultsViewModel _selectedResults; 421 422 private ResultsViewModel SelectedResults 423 { 424 get 425 { 426 return _selectedResults; 427 } 428 429 set 430 { 431 _selectedResults = value; 432 if (SelectedIsFromQueryResults()) 433 { 434 ContextMenu.Visibility = Visibility.Collapsed; 435 History.Visibility = Visibility.Collapsed; 436 ChangeQueryText(_queryTextBeforeLeaveResults); 437 } 438 else 439 { 440 Results.Visibility = Visibility.Collapsed; 441 _queryTextBeforeLeaveResults = QueryText; 442 } 443 444 _selectedResults.Visibility = Visibility.Collapsed; 445 } 446 } 447 448 public bool LoadedAtLeastOnce { get; set; } 449 450 private Visibility _visibility; 451 452 public Visibility MainWindowVisibility 453 { 454 get 455 { 456 return _visibility; 457 } 458 459 set 460 { 461 if (_visibility != value) 462 { 463 _visibility = value; 464 if (LoadedAtLeastOnce) 465 { 466 // Don't trigger telemetry on cold boot. Must have been loaded at least once. 467 if (value == Visibility.Visible) 468 { 469 PowerToysTelemetry.Log.WriteEvent(new LauncherShowEvent(_settings.Hotkey)); 470 } 471 else 472 { 473 PowerToysTelemetry.Log.WriteEvent(new LauncherHideEvent()); 474 } 475 } 476 477 OnPropertyChanged(nameof(MainWindowVisibility)); 478 } 479 } 480 } 481 482 public ICommand IgnoreCommand { get; private set; } 483 484 public ICommand EscCommand { get; private set; } 485 486 public ICommand SelectNextItemCommand { get; private set; } 487 488 public ICommand SelectPrevItemCommand { get; private set; } 489 490 public ICommand SelectNextContextMenuItemCommand { get; private set; } 491 492 public ICommand SelectPreviousContextMenuItemCommand { get; private set; } 493 494 public ICommand SelectNextTabItemCommand { get; private set; } 495 496 public ICommand SelectPrevTabItemCommand { get; private set; } 497 498 public ICommand SelectNextPageCommand { get; private set; } 499 500 public ICommand SelectPrevPageCommand { get; private set; } 501 502 public ICommand OpenResultWithKeyboardCommand { get; private set; } 503 504 public ICommand OpenResultWithMouseCommand { get; private set; } 505 506 public ICommand ClearQueryCommand { get; private set; } 507 508 public ICommand SelectNextOverviewPluginCommand { get; private set; } 509 510 public ICommand SelectPrevOverviewPluginCommand { get; private set; } 511 512 public class QueryTuningOptions 513 { 514 public int SearchClickedItemWeight { get; set; } 515 516 public bool SearchQueryTuningEnabled { get; set; } 517 518 public bool SearchWaitForSlowResults { get; set; } 519 } 520 521 public void Query() 522 { 523 Query(null); 524 } 525 526 public void Query(bool? delayedExecution) 527 { 528 if (SelectedIsFromQueryResults()) 529 { 530 QueryResults(delayedExecution); 531 } 532 else if (HistorySelected()) 533 { 534 QueryHistory(); 535 } 536 } 537 538 private void QueryHistory() 539 { 540 // Using CurrentCulture since query is received from user and used in downstream comparisons using CurrentCulture 541 var query = QueryText.ToLower(CultureInfo.CurrentCulture).Trim(); 542 History.Clear(); 543 544 var results = new List<Result>(); 545 foreach (var h in _history.Items) 546 { 547 var title = Properties.Resources.executeQuery; 548 var time = Properties.Resources.lastExecuteTime; 549 var result = new Result 550 { 551 Title = string.Format(CultureInfo.InvariantCulture, title, h.Query), 552 SubTitle = string.Format(CultureInfo.InvariantCulture, time, h.ExecutedDateTime), 553 IcoPath = "Images\\history.png", 554 OriginQuery = new Query(h.Query), 555 Action = _ => 556 { 557 SelectedResults = Results; 558 ChangeQueryText(h.Query); 559 return false; 560 }, 561 }; 562 results.Add(result); 563 } 564 565 if (!string.IsNullOrEmpty(query)) 566 { 567 var filtered = results.Where( 568 r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || 569 StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet()).ToList(); 570 571 History.AddResults(filtered, _updateToken); 572 } 573 else 574 { 575 History.AddResults(results, _updateToken); 576 } 577 } 578 579 private void QueryResults() 580 { 581 QueryResults(null); 582 } 583 584 private void QueryResults(bool? delayedExecution) 585 { 586 var queryTuning = GetQueryTuningOptions(); 587 var doFinalSort = queryTuning.SearchQueryTuningEnabled && queryTuning.SearchWaitForSlowResults; 588 589 if (!string.IsNullOrEmpty(QueryText)) 590 { 591 var queryTimer = new System.Diagnostics.Stopwatch(); 592 queryTimer.Start(); 593 _updateSource?.Cancel(); 594 _updateSource?.Dispose(); 595 var currentUpdateSource = new CancellationTokenSource(); 596 _updateSource = currentUpdateSource; 597 _updateToken = _updateSource.Token; 598 var queryText = QueryText.Trim(); 599 600 var pluginQueryPairs = QueryBuilder.Build(queryText); 601 if (pluginQueryPairs != null && pluginQueryPairs.Count > 0) 602 { 603 queryText = pluginQueryPairs.Values.First().RawQuery; 604 _currentQuery = queryText; 605 606 var queryResultsTask = Task.Factory.StartNew( 607 () => 608 { 609 Thread.Sleep(20); 610 611 // Keep track of total number of results for telemetry 612 var numResults = 0; 613 614 // Contains all the plugins for which this raw query is valid 615 var plugins = pluginQueryPairs.Keys.ToList(); 616 617 var sw = System.Diagnostics.Stopwatch.StartNew(); 618 619 try 620 { 621 var resultPluginPair = new System.Collections.Concurrent.ConcurrentDictionary<PluginMetadata, List<Result>>(); 622 623 if (_settings.PTRunNonDelayedSearchInParallel) 624 { 625 Parallel.ForEach(pluginQueryPairs, (pluginQueryItem) => 626 { 627 try 628 { 629 var plugin = pluginQueryItem.Key; 630 var query = pluginQueryItem.Value; 631 query.SelectedItems = _userSelectedRecord.GetGenericHistory(); 632 var results = PluginManager.QueryForPlugin(plugin, query); 633 resultPluginPair[plugin.Metadata] = results; 634 _updateToken.ThrowIfCancellationRequested(); 635 } 636 catch (OperationCanceledException) 637 { 638 // nothing to do here 639 } 640 }); 641 sw.Stop(); 642 } 643 else 644 { 645 _updateToken.ThrowIfCancellationRequested(); 646 647 // To execute a query corresponding to each plugin 648 foreach (KeyValuePair<PluginPair, Query> pluginQueryItem in pluginQueryPairs) 649 { 650 var plugin = pluginQueryItem.Key; 651 var query = pluginQueryItem.Value; 652 query.SelectedItems = _userSelectedRecord.GetGenericHistory(); 653 var results = PluginManager.QueryForPlugin(plugin, query); 654 resultPluginPair[plugin.Metadata] = results; 655 _updateToken.ThrowIfCancellationRequested(); 656 } 657 } 658 659 lock (_addResultsLock) 660 { 661 // Using CurrentCultureIgnoreCase since this is user facing 662 if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) 663 { 664 Results.Clear(); 665 foreach (var p in resultPluginPair) 666 { 667 UpdateResultView(p.Value, queryText, _updateToken); 668 _updateToken.ThrowIfCancellationRequested(); 669 } 670 671 _updateToken.ThrowIfCancellationRequested(); 672 numResults = Results.Results.Count; 673 if (!doFinalSort) 674 { 675 Results.Sort(queryTuning); 676 Results.SelectedItem = Results.Results.FirstOrDefault(); 677 } 678 } 679 680 _updateToken.ThrowIfCancellationRequested(); 681 if (!doFinalSort) 682 { 683 UpdateResultsListViewAfterQuery(queryText); 684 } 685 } 686 687 bool noInitialResults = numResults == 0; 688 689 if (!delayedExecution.HasValue || delayedExecution.Value) 690 { 691 // Run the slower query of the DelayedExecution plugins 692 _updateToken.ThrowIfCancellationRequested(); 693 Parallel.ForEach(plugins, (plugin) => 694 { 695 try 696 { 697 Query query; 698 pluginQueryPairs.TryGetValue(plugin, out query); 699 var results = PluginManager.QueryForPlugin(plugin, query, true); 700 _updateToken.ThrowIfCancellationRequested(); 701 if ((results?.Count ?? 0) != 0) 702 { 703 lock (_addResultsLock) 704 { 705 // Using CurrentCultureIgnoreCase since this is user facing 706 if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) 707 { 708 _updateToken.ThrowIfCancellationRequested(); 709 710 // Remove the original results from the plugin 711 Results.Results.RemoveAll(r => r.Result.PluginID == plugin.Metadata.ID); 712 _updateToken.ThrowIfCancellationRequested(); 713 714 // Add the new results from the plugin 715 UpdateResultView(results, queryText, _updateToken); 716 717 _updateToken.ThrowIfCancellationRequested(); 718 numResults = Results.Results.Count; 719 if (!doFinalSort) 720 { 721 Results.Sort(queryTuning); 722 } 723 } 724 725 _updateToken.ThrowIfCancellationRequested(); 726 if (!doFinalSort) 727 { 728 UpdateResultsListViewAfterQuery(queryText, noInitialResults, true); 729 } 730 } 731 } 732 } 733 catch (OperationCanceledException) 734 { 735 // nothing to do here 736 } 737 }); 738 } 739 } 740 catch (OperationCanceledException) 741 { 742 // nothing to do here 743 } 744 745 queryTimer.Stop(); 746 var queryEvent = new LauncherQueryEvent() 747 { 748 QueryTimeMs = queryTimer.ElapsedMilliseconds, 749 NumResults = numResults, 750 QueryLength = queryText.Length, 751 }; 752 PowerToysTelemetry.Log.WriteEvent(queryEvent); 753 }, 754 _updateToken); 755 756 if (doFinalSort) 757 { 758 Task.Factory.ContinueWhenAll( 759 new Task[] { queryResultsTask }, 760 completedTasks => 761 { 762 Results.Sort(queryTuning); 763 Results.SelectedItem = Results.Results.FirstOrDefault(); 764 UpdateResultsListViewAfterQuery(queryText, false, false); 765 }, 766 _updateToken); 767 } 768 } 769 } 770 else 771 { 772 _updateSource?.Cancel(); 773 _currentQuery = _emptyQuery; 774 Results.SelectedItem = null; 775 Results.Visibility = Visibility.Collapsed; 776 Task.Run(() => 777 { 778 lock (_addResultsLock) 779 { 780 Results.Clear(); 781 } 782 }); 783 } 784 } 785 786 private void UpdateResultsListViewAfterQuery(string queryText, bool noInitialResults = false, bool isDelayedInvoke = false) 787 { 788 Application.Current.Dispatcher.BeginInvoke(new Action(() => 789 { 790 lock (_addResultsLock) 791 { 792 // Using CurrentCultureIgnoreCase since this is user facing 793 if (queryText.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) 794 { 795 Results.Results.NotifyChanges(); 796 } 797 798 if (Results.Results.Count > 0) 799 { 800 Results.Visibility = Visibility.Visible; 801 if (!isDelayedInvoke || noInitialResults) 802 { 803 Results.SelectedIndex = 0; 804 if (noInitialResults) 805 { 806 Results.SelectedItem = Results.Results.FirstOrDefault(); 807 } 808 } 809 } 810 else 811 { 812 Results.Visibility = Visibility.Collapsed; 813 } 814 } 815 })); 816 } 817 818 private bool SelectedIsFromQueryResults() 819 { 820 var selected = SelectedResults == Results; 821 return selected; 822 } 823 824 private bool HistorySelected() 825 { 826 var selected = SelectedResults == History; 827 return selected; 828 } 829 830 internal bool ProcessHotKeyMessages(IntPtr wparam, IntPtr lparam) 831 { 832 if (wparam.ToInt32() == _globalHotKeyId) 833 { 834 OnHotkey(); 835 return true; 836 } 837 838 return false; 839 } 840 841 private static uint VKModifiersFromHotKey(Hotkey hotkey) 842 { 843 return (uint)(HOTKEY_MODIFIERS.NOREPEAT | (hotkey.Alt ? HOTKEY_MODIFIERS.ALT : 0) | (hotkey.Ctrl ? HOTKEY_MODIFIERS.CONTROL : 0) | (hotkey.Shift ? HOTKEY_MODIFIERS.SHIFT : 0) | (hotkey.Win ? HOTKEY_MODIFIERS.WIN : 0)); 844 } 845 846 private void SetHotkey(IntPtr hwnd, string hotkeyStr, HotkeyCallback action) 847 { 848 var hotkey = new HotkeyModel(hotkeyStr); 849 SetHotkey(hwnd, hotkey, action); 850 } 851 852 private void SetHotkey(IntPtr hwnd, HotkeyModel hotkeyModel, HotkeyCallback action) 853 { 854 Log.Info("Set HotKey()", GetType()); 855 string hotkeyStr = hotkeyModel.ToString(); 856 857 try 858 { 859 Hotkey hotkey = new Hotkey 860 { 861 Alt = hotkeyModel.Alt, 862 Shift = hotkeyModel.Shift, 863 Ctrl = hotkeyModel.Ctrl, 864 Win = hotkeyModel.Win, 865 Key = (byte)KeyInterop.VirtualKeyFromKey(hotkeyModel.CharKey), 866 }; 867 868 if (_usingGlobalHotKey) 869 { 870 NativeMethods.UnregisterHotKey(_globalHotKeyHwnd, _globalHotKeyId); 871 _usingGlobalHotKey = false; 872 Log.Info("Unregistering previous global hotkey", GetType()); 873 } 874 875 if (_hotkeyHandle != 0) 876 { 877 HotkeyManager?.UnregisterHotkey(_hotkeyHandle); 878 _hotkeyHandle = 0; 879 Log.Info("Unregistering previous low level key handler", GetType()); 880 } 881 882 if (_settings.StartedFromPowerToysRunner && _settings.UseCentralizedKeyboardHook) 883 { 884 Log.Info("Using the Centralized Keyboard Hook for the HotKey.", GetType()); 885 } 886 else 887 { 888 _globalHotKeyVK = hotkey.Key; 889 _globalHotKeyFSModifiers = VKModifiersFromHotKey(hotkey); 890 if (NativeMethods.RegisterHotKey(hwnd, _globalHotKeyId, _globalHotKeyFSModifiers, _globalHotKeyVK)) 891 { 892 // Using global hotkey registered through the native RegisterHotKey method. 893 _globalHotKeyHwnd = hwnd; 894 _usingGlobalHotKey = true; 895 Log.Info("Registered global hotkey", GetType()); 896 return; 897 } 898 899 Log.Warn("Registering global shortcut failed. Will use low-level keyboard hook instead.", GetType()); 900 901 // Using fallback low-level keyboard hook through HotkeyManager. 902 if (HotkeyManager == null) 903 { 904 HotkeyManager = new HotkeyManager(); 905 } 906 907 _hotkeyHandle = HotkeyManager.RegisterHotkey(hotkey, action); 908 } 909 } 910 catch (Exception) 911 { 912 string errorMsg = string.Format(CultureInfo.InvariantCulture, RegisterHotkeyFailed, hotkeyStr); 913 MessageBox.Show(errorMsg, Properties.Resources.RegisterHotkeyFailedTitle); 914 } 915 } 916 917 /// <summary> 918 /// Checks if Wox should ignore any hotkeys 919 /// </summary> 920 /// <returns>if any hotkeys should be ignored</returns> 921 private bool ShouldIgnoreHotkeys() 922 { 923 // double if to omit calling win32 function 924 if (_settings.IgnoreHotkeysOnFullscreen) 925 { 926 if (WindowsInteropHelper.IsWindowFullscreen()) 927 { 928 return true; 929 } 930 } 931 932 return false; 933 } 934 935 /* TODO: Custom Hotkeys for Plugins. Commented since this is an incomplete feature. 936 * This needs: 937 * - Support for use with global shortcut. 938 * - Support for use with the fallback Shortcut Manager. 939 * - Support for use through the runner centralized keyboard hooks. 940 private void SetCustomPluginHotkey() 941 { 942 if (_settings.CustomPluginHotkeys == null) 943 { 944 return; 945 } 946 947 foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys) 948 { 949 SetHotkey(hotkey.Hotkey, () => 950 { 951 if (ShouldIgnoreHotkeys()) 952 { 953 return; 954 } 955 956 MainWindowVisibility = Visibility.Visible; 957 ChangeQueryText(hotkey.ActionKeyword); 958 }); 959 } 960 } 961 */ 962 963 private void OnCentralizedKeyboardHookHotKey() 964 { 965 if (_settings.StartedFromPowerToysRunner && _settings.UseCentralizedKeyboardHook) 966 { 967 OnHotkey(); 968 } 969 } 970 971 private void OnHotkey() 972 { 973 Application.Current.Dispatcher.Invoke(() => 974 { 975 Log.Info("OnHotkey", MethodBase.GetCurrentMethod().DeclaringType); 976 if (!ShouldIgnoreHotkeys()) 977 { 978 // If launcher window was hidden and the hotkey was pressed, start telemetry event 979 if (MainWindowVisibility != Visibility.Visible) 980 { 981 StartHotkeyTimer(); 982 } 983 984 if (_settings.LastQueryMode == LastQueryMode.Empty) 985 { 986 ChangeQueryText(string.Empty); 987 } 988 else if (_settings.LastQueryMode == LastQueryMode.Preserved) 989 { 990 LastQuerySelected = true; 991 } 992 else if (_settings.LastQueryMode == LastQueryMode.Selected) 993 { 994 LastQuerySelected = false; 995 } 996 else 997 { 998 throw new ArgumentException($"wrong LastQueryMode: <{_settings.LastQueryMode}>"); 999 } 1000 1001 ToggleWox(); 1002 } 1003 }); 1004 } 1005 1006 public void ToggleWox() 1007 { 1008 if (MainWindowVisibility != Visibility.Visible) 1009 { 1010 MainWindowVisibility = Visibility.Visible; 1011 } 1012 else 1013 { 1014 if (_settings.ClearInputOnLaunch) 1015 { 1016 ClearQueryCommand.Execute(null); 1017 Task.Run(() => 1018 { 1019 Thread.Sleep(100); 1020 Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => 1021 { 1022 MainWindowVisibility = Visibility.Collapsed; 1023 })); 1024 }); 1025 } 1026 else 1027 { 1028 MainWindowVisibility = Visibility.Collapsed; 1029 } 1030 } 1031 } 1032 1033 public void Hide() 1034 { 1035 if (MainWindowVisibility != Visibility.Collapsed) 1036 { 1037 ToggleWox(); 1038 } 1039 } 1040 1041 public void Save() 1042 { 1043 if (!_saved) 1044 { 1045 if (_historyItemsStorage.CheckVersionMismatch()) 1046 { 1047 if (!_historyItemsStorage.TryLoadData()) 1048 { 1049 _history.Update(); 1050 } 1051 } 1052 1053 _historyItemsStorage.Save(); 1054 1055 if (_userSelectedRecordStorage.CheckVersionMismatch()) 1056 { 1057 if (!_userSelectedRecordStorage.TryLoadData()) 1058 { 1059 _userSelectedRecord.Update(); 1060 } 1061 } 1062 1063 _userSelectedRecordStorage.Save(); 1064 1065 _saved = true; 1066 } 1067 } 1068 1069 /// <summary> 1070 /// To avoid deadlock, this method should not called from main thread 1071 /// </summary> 1072 public void UpdateResultView(List<Result> list, string originQuery, CancellationToken ct) 1073 { 1074 ArgumentNullException.ThrowIfNull(list); 1075 1076 ArgumentNullException.ThrowIfNull(originQuery); 1077 1078 foreach (var result in list) 1079 { 1080 var selectedData = _userSelectedRecord.GetSelectedData(result); 1081 result.SelectedCount = selectedData.SelectedCount; 1082 result.LastSelected = selectedData.LastSelected; 1083 } 1084 1085 // Using CurrentCultureIgnoreCase since this is user facing 1086 if (originQuery.Equals(_currentQuery, StringComparison.CurrentCultureIgnoreCase)) 1087 { 1088 ct.ThrowIfCancellationRequested(); 1089 Results.AddResults(list, ct); 1090 } 1091 } 1092 1093 public void HandleContextMenu(Key acceleratorKey, ModifierKeys acceleratorModifiers) 1094 { 1095 var results = SelectedResults; 1096 if (results.SelectedItem != null) 1097 { 1098 foreach (ContextMenuItemViewModel contextMenuItems in results.SelectedItem.ContextMenuItems) 1099 { 1100 if (contextMenuItems.AcceleratorKey == acceleratorKey && contextMenuItems.AcceleratorModifiers == acceleratorModifiers) 1101 { 1102 contextMenuItems.Command.Execute(null); 1103 } 1104 } 1105 } 1106 } 1107 1108 public static bool ShouldAutoCompleteTextBeEmpty(string queryText, string autoCompleteText) 1109 { 1110 if (string.IsNullOrEmpty(autoCompleteText)) 1111 { 1112 return false; 1113 } 1114 else 1115 { 1116 // Using Ordinal this is internal 1117 return string.IsNullOrEmpty(queryText) || !autoCompleteText.StartsWith(queryText, StringComparison.Ordinal); 1118 } 1119 } 1120 1121 public static string GetAutoCompleteText(int index, string input, string query) 1122 { 1123 if (!string.IsNullOrEmpty(input) && !string.IsNullOrEmpty(query)) 1124 { 1125 if (index == 0) 1126 { 1127 // Using OrdinalIgnoreCase because we want the characters to be exact in autocomplete text and the query 1128 if (input.StartsWith(query, StringComparison.OrdinalIgnoreCase)) 1129 { 1130 // Use the same case as the input query for the matched portion of the string 1131 return string.Concat(query, input.AsSpan(query.Length)); 1132 } 1133 } 1134 } 1135 1136 return string.Empty; 1137 } 1138 1139 public static string GetSearchText(int index, string input, string query) 1140 { 1141 if (!string.IsNullOrEmpty(input)) 1142 { 1143 if (index == 0 && !string.IsNullOrEmpty(query)) 1144 { 1145 // Using OrdinalIgnoreCase since this is internal 1146 if (input.StartsWith(query, StringComparison.OrdinalIgnoreCase)) 1147 { 1148 return string.Concat(query, input.AsSpan(query.Length)); 1149 } 1150 } 1151 1152 return input; 1153 } 1154 1155 return string.Empty; 1156 } 1157 1158 public static FlowDirection GetLanguageFlowDirection() 1159 { 1160 try 1161 { 1162 bool isCurrentLanguageRightToLeft = System.Windows.Input.InputLanguageManager.Current.CurrentInputLanguage.TextInfo.IsRightToLeft; 1163 1164 return isCurrentLanguageRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; 1165 } 1166 catch (CultureNotFoundException ex) 1167 { 1168 Log.Exception($"CultureNotFoundException: {ex.Message}", ex, MethodBase.GetCurrentMethod().DeclaringType); 1169 return FlowDirection.LeftToRight; // default FlowDirection.LeftToRight 1170 } 1171 } 1172 1173 protected virtual void Dispose(bool disposing) 1174 { 1175 if (!_disposed) 1176 { 1177 if (disposing) 1178 { 1179 if (_usingGlobalHotKey) 1180 { 1181 NativeMethods.UnregisterHotKey(_globalHotKeyHwnd, _globalHotKeyId); 1182 _usingGlobalHotKey = false; 1183 } 1184 1185 if (_hotkeyHandle != 0) 1186 { 1187 HotkeyManager?.UnregisterHotkey(_hotkeyHandle); 1188 } 1189 1190 HotkeyManager?.Dispose(); 1191 _updateSource?.Dispose(); 1192 _disposed = true; 1193 } 1194 } 1195 } 1196 1197 public void Dispose() 1198 { 1199 Dispose(disposing: true); 1200 GC.SuppressFinalize(this); 1201 } 1202 1203 public void StartHotkeyTimer() 1204 { 1205 _hotkeyTimer.Start(); 1206 } 1207 1208 public long GetHotkeyEventTimeMs() 1209 { 1210 _hotkeyTimer.Stop(); 1211 long recordedTime = _hotkeyTimer.ElapsedMilliseconds; 1212 1213 // Reset the stopwatch and return the time elapsed 1214 _hotkeyTimer.Reset(); 1215 return recordedTime; 1216 } 1217 1218 public bool GetSearchQueryResultsWithDelaySetting() 1219 { 1220 return _settings.SearchQueryResultsWithDelay; 1221 } 1222 1223 public int GetSearchInputDelayFastSetting() 1224 { 1225 return _settings.SearchInputDelayFast; 1226 } 1227 1228 public int GetSearchInputDelaySetting() 1229 { 1230 return _settings.SearchInputDelay; 1231 } 1232 1233 public QueryTuningOptions GetQueryTuningOptions() 1234 { 1235 return new MainViewModel.QueryTuningOptions 1236 { 1237 SearchClickedItemWeight = GetSearchClickedItemWeight(), 1238 SearchQueryTuningEnabled = GetSearchQueryTuningEnabled(), 1239 SearchWaitForSlowResults = GetSearchWaitForSlowResults(), 1240 }; 1241 } 1242 1243 public int GetSearchClickedItemWeight() 1244 { 1245 return _settings.SearchClickedItemWeight; 1246 } 1247 1248 public bool GetSearchQueryTuningEnabled() 1249 { 1250 return _settings.SearchQueryTuningEnabled; 1251 } 1252 1253 public bool GetSearchWaitForSlowResults() 1254 { 1255 return _settings.SearchWaitForSlowResults; 1256 } 1257 1258 public static void PerformSafeAction(Action action) 1259 { 1260 lock (_addResultsLock) 1261 { 1262 action.Invoke(); 1263 } 1264 } 1265 1266 public ObservableCollection<PluginPair> Plugins { get; } = new(); 1267 1268 private PluginPair _selectedPlugin; 1269 1270 public PluginPair SelectedPlugin 1271 { 1272 get => _selectedPlugin; 1273 1274 set 1275 { 1276 if (_selectedPlugin != value) 1277 { 1278 _selectedPlugin = value; 1279 OnPropertyChanged(nameof(SelectedPlugin)); 1280 } 1281 } 1282 } 1283 1284 private Visibility _pluginsOverviewVisibility = Visibility.Visible; 1285 1286 public Visibility PluginsOverviewVisibility 1287 { 1288 get => _pluginsOverviewVisibility; 1289 1290 set 1291 { 1292 if (_pluginsOverviewVisibility != value) 1293 { 1294 _pluginsOverviewVisibility = value; 1295 OnPropertyChanged(nameof(PluginsOverviewVisibility)); 1296 } 1297 } 1298 } 1299 1300 public void RefreshPluginsOverview() 1301 { 1302 Log.Info("Refresh plugins overview", GetType()); 1303 1304 Application.Current.Dispatcher.Invoke(() => 1305 { 1306 Plugins.Clear(); 1307 1308 foreach (var p in PluginManager.AllPlugins.Where(a => a.IsPluginInitialized && !a.Metadata.Disabled && a.Metadata.ActionKeyword != string.Empty)) 1309 { 1310 if (_settings.ShowPluginsOverview == PowerToysRunSettings.ShowPluginsOverviewMode.All || (_settings.ShowPluginsOverview == PowerToysRunSettings.ShowPluginsOverviewMode.NonGlobal && !p.Metadata.IsGlobal)) 1311 { 1312 Plugins.Add(p); 1313 } 1314 } 1315 }); 1316 } 1317 1318 private void SelectNextOverviewPlugin() 1319 { 1320 if (Plugins.Count == 0) 1321 { 1322 return; 1323 } 1324 1325 var selectedIndex = Plugins.IndexOf(SelectedPlugin); 1326 if (selectedIndex == -1) 1327 { 1328 selectedIndex = 0; 1329 } 1330 else 1331 { 1332 if (++selectedIndex > Plugins.Count - 1) 1333 { 1334 selectedIndex = 0; 1335 } 1336 } 1337 1338 SelectedPlugin = Plugins[selectedIndex]; 1339 } 1340 1341 private void SelectPrevOverviewPlugin() 1342 { 1343 if (Plugins.Count == 0) 1344 { 1345 return; 1346 } 1347 1348 var selectedIndex = Plugins.IndexOf(SelectedPlugin); 1349 if (selectedIndex == -1) 1350 { 1351 selectedIndex = Plugins.Count - 1; 1352 } 1353 else 1354 { 1355 if (--selectedIndex < 0) 1356 { 1357 selectedIndex = Plugins.Count - 1; 1358 } 1359 } 1360 1361 SelectedPlugin = Plugins[selectedIndex]; 1362 } 1363 } 1364 }