/ src / modules / launcher / PowerLauncher / ViewModel / MainViewModel.cs
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  }