Files
PowerToys/src/modules/launcher/PowerLauncher/MainWindow.xaml.cs

890 lines
37 KiB
C#
Raw Normal View History

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
2021-03-19 19:03:12 +02:00
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Common.UI;
using ManagedCommon;
using Microsoft.PowerLauncher.Telemetry;
using Microsoft.PowerToys.Telemetry;
using PowerLauncher.Helper;
2021-03-19 19:03:12 +02:00
using PowerLauncher.Plugin;
using PowerLauncher.Telemetry.Events;
using PowerLauncher.ViewModel;
using PowerToys.Interop;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
using Wox.Plugin.Interfaces;
using CancellationToken = System.Threading.CancellationToken;
using Image = Wox.Infrastructure.Image;
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
using Log = Wox.Plugin.Logger.Log;
using Screen = System.Windows.Forms.Screen;
namespace PowerLauncher
{
public partial class MainWindow : IDisposable
{
[fxcop] Wox.Infrastructure (#7590) * CA1052: Static holder types should be Static or NotInheritable * CA1041: Provide ObsoleteAttribute message * CA1062: Validate arguments of public methods * CA1304: Specify CultureInfo / CA1305: Specify IFormatProvider / CA1307: Specify StringComparison for clarity * CA1802: Use Literals Where Appropriate * CA1820: Test for empty strings using string length * CA1707: Identifiers should not contain underscores * CA1805: Do not initialize unnecessarily. * CA1822: Mark members as static * CA2227: Collection properties should be read only * CA1054: URI parameters should not be strings * CA1031: Do not catch general exception types * CA1060: Move P/Invokes to NativeMethods class * CA1308: Normalize strings to uppercase * CA2000: Dispose objects before losing scope / CA2234: Pass System.Uri objects instead of strings * CA2234: Pass System.Uri objects instead of strings * CA1044: Properties should not be write only * CA1716: Identifiers should not match keywords * CA2007: Do not directly await a Task * CA2007: Do not directly await a Task (Suppressed) * CA5350: Do Not Use Weak Cryptographic Algorithms (Suppressed) * CA1724: Type names should not match namespaces (renamed Settings.cs to PowerToysRunSettings.cs) * CA1033: Interface methods should be callable by child types (Added sealed modifier to class) * CA1724: Type names should not match namespaces (Renamed Plugin.cs to RunPlugin.cs) * CA1724: Type names should not match namespaces (Renamed Http.cs to HttpClient.cs) * CA5364: Do not use deprecated security protocols (Remove unused code) * Enabled FxCopAnalyzer for Wox.Infrastructure * fixed comment * Addressed comments - Changed Ordinal to InvariantCulture - Added comments - Removed unused obsolete code - Removed unused method (CA2007: Do not directly await a Task) * Addressed comments - fixed justification for CA1031 suppression * Addressed comments - Fixed justification for CA1031 suppression in Wox.Core/Wox.Plugin
2020-10-29 17:52:35 -07:00
private readonly PowerToysRunSettings _settings;
private readonly MainViewModel _viewModel;
private readonly CancellationToken _nativeWaiterCancelToken;
private bool _isTextSetProgrammatically;
2020-08-21 12:40:31 -07:00
private bool _deletePressed;
private HwndSource _hwndSource;
private Timer _firstDeleteTimer = new Timer();
2020-08-21 12:40:31 -07:00
private bool _coldStateHotkeyPressed;
private bool _disposedValue;
private IDisposable _reactiveSubscription;
private Point _mouseDownPosition;
private ResultViewModel _mouseDownResultViewModel;
// The enum flag for DwmSetWindowAttribute's second parameter, which tells the function what attribute to set.
public enum DWMWINDOWATTRIBUTE
{
DWMWA_WINDOW_CORNER_PREFERENCE = 33,
}
// The DWM_WINDOW_CORNER_PREFERENCE enum for DwmSetWindowAttribute's third parameter, which tells the function
// what value of the enum to set.
// Copied from dwmapi.h
public enum DWM_WINDOW_CORNER_PREFERENCE
{
DWMWCP_DEFAULT = 0,
DWMWCP_DONOTROUND = 1,
DWMWCP_ROUND = 2,
DWMWCP_ROUNDSMALL = 3,
}
// Import dwmapi.dll and define DwmSetWindowAttribute in C# corresponding to the native function.
[DllImport("dwmapi.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
internal static extern void DwmSetWindowAttribute(
IntPtr hwnd,
DWMWINDOWATTRIBUTE attribute,
ref DWM_WINDOW_CORNER_PREFERENCE pvAttribute,
uint cbAttribute);
public MainWindow(PowerToysRunSettings settings, MainViewModel mainVM, CancellationToken nativeWaiterCancelToken)
: this()
{
DataContext = mainVM;
_viewModel = mainVM;
_nativeWaiterCancelToken = nativeWaiterCancelToken;
_settings = settings;
// Fixes #30850
AppContext.SetSwitch("Switch.System.Windows.Controls.Grid.StarDefinitionsCanExceedAvailableSpace", true);
InitializeComponent();
_firstDeleteTimer.Elapsed += CheckForFirstDelete;
_firstDeleteTimer.Interval = 1000;
NativeEventWaiter.WaitForEventLoop(
Constants.RunSendSettingsTelemetryEvent(),
SendSettingsTelemetry,
Application.Current.Dispatcher,
_nativeWaiterCancelToken);
2021-03-19 19:03:12 +02:00
}
private void SendSettingsTelemetry()
{
try
2021-03-19 19:03:12 +02:00
{
Log.Info("Send Run settings telemetry", this.GetType());
var plugins = PluginManager.AllPlugins.ToDictionary(x => x.Metadata.Name + " " + x.Metadata.ID, x => new PluginModel()
{
ID = x.Metadata.ID,
Name = x.Metadata.Name,
Disabled = x.Metadata.Disabled,
ActionKeyword = x.Metadata.ActionKeyword,
IsGlobal = x.Metadata.IsGlobal,
});
2021-03-19 19:03:12 +02:00
var telemetryEvent = new RunPluginsSettingsEvent(plugins);
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
}
catch (Exception ex)
{
Log.Exception("Unhandled exception when trying to send PowerToys Run settings telemetry.", ex, GetType());
}
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
WindowsInteropHelper.SetToolWindowStyle(this);
}
private void CheckForFirstDelete(object sender, ElapsedEventArgs e)
{
if (_firstDeleteTimer != null)
{
_firstDeleteTimer.Stop();
if (_deletePressed)
{
PowerToysTelemetry.Log.WriteEvent(new LauncherFirstDeleteEvent());
}
}
}
public MainWindow()
{
InitializeComponent();
}
private void OnClosing(object sender, CancelEventArgs e)
{
_viewModel.Save();
}
private void BringProcessToForeground()
{
// Use SendInput hack to allow Activate to work - required to resolve focus issue https://github.com/microsoft/PowerToys/issues/4270
WindowsInteropHelper.INPUT input = new WindowsInteropHelper.INPUT { Type = WindowsInteropHelper.INPUTTYPE.INPUTMOUSE, Data = { } };
WindowsInteropHelper.INPUT[] inputs = new WindowsInteropHelper.INPUT[] { input };
// Send empty mouse event. This makes this thread the last to send input, and hence allows it to pass foreground permission checks
_ = NativeMethods.SendInput(1, inputs, WindowsInteropHelper.INPUT.Size);
Activate();
}
private const string EnvironmentChangeType = "Environment";
public IntPtr ProcessWindowMessages(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled)
{
switch ((WM)msg)
{
case WM.SETTINGCHANGE:
string changeType = Marshal.PtrToStringUni(lparam);
if (changeType == EnvironmentChangeType)
{
Log.Info("Reload environment: Updating environment variables for PT Run's process", typeof(EnvironmentHelper));
EnvironmentHelper.UpdateEnvironment();
handled = true;
}
break;
case WM.HOTKEY:
handled = _viewModel.ProcessHotKeyMessages(wparam, lparam);
break;
}
return IntPtr.Zero;
}
private void OnSourceInitialized(object sender, EventArgs e)
{
// Initialize protected environment variables before register the WindowMessage
EnvironmentHelper.GetProtectedEnvironmentVariables();
_hwndSource = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
_hwndSource.AddHook(ProcessWindowMessages);
// Call RegisterHotKey only after a window handle can be used, so that a global hotkey can be registered.
_viewModel.RegisterHotkey(_hwndSource.Handle);
if (OSVersionHelper.IsGreaterThanWindows11_21H2())
{
// ResizeMode="NoResize" removes rounded corners. So force them to rounded.
IntPtr hWnd = new WindowInteropHelper(GetWindow(this)).EnsureHandle();
DWMWINDOWATTRIBUTE attribute = DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE;
DWM_WINDOW_CORNER_PREFERENCE preference = DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND;
DwmSetWindowAttribute(hWnd, attribute, ref preference, sizeof(uint));
}
else
{
// On Windows10 ResizeMode="NoResize" removes the border so we add a new one.
// Also on 22000 it crashes due to DWMWA_WINDOW_CORNER_PREFERENCE https://github.com/microsoft/PowerToys/issues/36558
MainBorder.BorderThickness = new System.Windows.Thickness(0.5);
}
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
WindowsInteropHelper.DisableControlBox(this);
InitializePosition();
SearchBox.QueryTextBox.DataContext = _viewModel;
SearchBox.QueryTextBox.PreviewKeyDown += Launcher_KeyDown;
SetupSearchTextBoxReactiveness(_viewModel.GetSearchQueryResultsWithDelaySetting());
_viewModel.RegisterSettingsChangeListener(
(s, prop_e) =>
{
if (prop_e.PropertyName == nameof(PowerToysRunSettings.SearchQueryResultsWithDelay) || prop_e.PropertyName == nameof(PowerToysRunSettings.SearchInputDelay) || prop_e.PropertyName == nameof(PowerToysRunSettings.SearchInputDelayFast))
{
Application.Current.Dispatcher.Invoke(() =>
{
SetupSearchTextBoxReactiveness(_viewModel.GetSearchQueryResultsWithDelaySetting());
});
}
});
// Set initial language flow direction
SearchBox_UpdateFlowDirection();
// Register language changed event
InputLanguageManager.Current.InputLanguageChanged += SearchBox_InputLanguageChanged;
SearchBox.QueryTextBox.Focus();
SearchBox.QueryTextBox.ControlledElements.Add(ListBox.SuggestionsList);
ListBox.DataContext = _viewModel;
ListBox.SuggestionsList.SelectionChanged += SuggestionsList_SelectionChanged;
ListBox.SuggestionsList.PreviewMouseLeftButtonUp += SuggestionsList_PreviewMouseLeftButtonUp;
ListBox.SuggestionsList.PreviewMouseLeftButtonDown += SuggestionsList_PreviewMouseLeftButtonDown;
ListBox.SuggestionsList.MouseMove += SuggestionsList_MouseMove;
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
_viewModel.MainWindowVisibility = Visibility.Collapsed;
_viewModel.LoadedAtLeastOnce = true;
_viewModel.SetPluginsOverviewVisibility();
_viewModel.SetFontSize();
BringProcessToForeground();
}
private void SetupSearchTextBoxReactiveness(bool showResultsWithDelay)
{
if (_reactiveSubscription != null)
{
_reactiveSubscription.Dispose();
_reactiveSubscription = null;
}
SearchBox.QueryTextBox.TextChanged -= QueryTextBox_TextChanged;
if (showResultsWithDelay)
{
_reactiveSubscription = Observable.FromEventPattern<TextChangedEventHandler, TextChangedEventWithInitiatorArgs>(
conversion => (sender, eventArg) => conversion(sender, new TextChangedEventWithInitiatorArgs(eventArg.RoutedEvent, eventArg.UndoAction)),
add => SearchBox.QueryTextBox.TextChanged += add,
remove => SearchBox.QueryTextBox.TextChanged -= remove)
.Do(@event => ClearAutoCompleteText((TextBox)@event.Sender, @event))
.Throttle(TimeSpan.FromMilliseconds(_settings.SearchInputDelayFast))
.Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, false, @event)))
.Throttle(TimeSpan.FromMilliseconds(_settings.SearchInputDelay))
.Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, true, @event)))
.Subscribe();
/*
if (_settings.PTRSearchQueryFastResultsWithDelay)
{
// old mode, delay fast and delayed execution
_reactiveSubscription = Observable.FromEventPattern<TextChangedEventHandler, TextChangedEventArgs>(
add => SearchBox.QueryTextBox.TextChanged += add,
remove => SearchBox.QueryTextBox.TextChanged -= remove)
.Do(@event => ClearAutoCompleteText((TextBox)@event.Sender))
.Throttle(TimeSpan.FromMilliseconds(searchInputDelayMs))
.Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender)))
.Subscribe();
}
else
{
if (_settings.PTRSearchQueryFastResultsWithPartialDelay)
{
// new mode, fire non-delayed right away, and then later the delayed execution
_reactiveSubscription = Observable.FromEventPattern<TextChangedEventHandler, TextChangedEventArgs>(
add => SearchBox.QueryTextBox.TextChanged += add,
remove => SearchBox.QueryTextBox.TextChanged -= remove)
.Do(@event => ClearAutoCompleteText((TextBox)@event.Sender))
.Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, false)))
.Throttle(TimeSpan.FromMilliseconds(searchInputDelayMs))
.Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, true)))
.Subscribe();
}
else
{
// new mode, fire non-delayed after short delay, and then later the delayed execution
_reactiveSubscription = Observable.FromEventPattern<TextChangedEventHandler, TextChangedEventArgs>(
add => SearchBox.QueryTextBox.TextChanged += add,
remove => SearchBox.QueryTextBox.TextChanged -= remove)
.Do(@event => ClearAutoCompleteText((TextBox)@event.Sender))
.Throttle(TimeSpan.FromMilliseconds(_settings.SearchInputDelayFast))
.Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, false)))
.Throttle(TimeSpan.FromMilliseconds(searchInputDelayMs))
.Do(@event => Dispatcher.InvokeAsync(() => PerformSearchQuery((TextBox)@event.Sender, true)))
.Subscribe();
}
}
*/
}
else
{
SearchBox.QueryTextBox.TextChanged += QueryTextBox_TextChanged;
}
}
private void SuggestionsList_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var result = ((FrameworkElement)e.OriginalSource).DataContext;
if (result != null)
{
Updates for check-spelling v0.0.25 (#40386) ## Summary of the Pull Request - #39572 updated check-spelling but ignored: > 🐣 Breaking Changes [Code Scanning action requires a Code Scanning Ruleset](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset) If you use SARIF reporting, then instead of the workflow yielding an ❌ when it fails, it will rely on [github-advanced-security 🤖](https://github.com/apps/github-advanced-security) to report the failure. You will need to adjust your checks for PRs. This means that check-spelling hasn't been properly doing its job 😦. I'm sorry, I should have pushed a thing to this repo earlier,... Anyway, as with most refreshes, this comes with a number of fixes, some are fixes for typos that snuck in before the 0.0.25 upgrade, some are for things that snuck in after, some are based on new rules in spell-check-this, and some are hand written patterns based on running through this repository a few times. About the 🐣 **breaking change**: someone needs to create a ruleset for this repository (see [Code Scanning action requires a Code Scanning Ruleset: Sample ruleset ](https://github.com/check-spelling/check-spelling/wiki/Breaking-Change:-Code-Scanning-action-requires-a-Code-Scanning-Ruleset#sample-ruleset)). The alternative to adding a ruleset is to change the condition to not use sarif for this repository. In general, I think the github integration from sarif is prettier/more helpful, so I think that it's the better choice. You can see an example of it working in: - https://github.com/check-spelling-sandbox/PowerToys/pull/23 --------- Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> Co-authored-by: Mike Griese <migrie@microsoft.com> Co-authored-by: Dustin L. Howett <dustin@howett.net>
2025-07-08 18:16:52 -04:00
// This may be null if the tapped item was one of the context buttons (run as admin, etc.).
if (result is ResultViewModel resultVM)
{
_viewModel.Results.SelectedItem = resultVM;
2020-09-14 13:12:02 -07:00
_viewModel.OpenResultWithMouseCommand.Execute(null);
}
}
}
private void SuggestionsList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_mouseDownPosition = e.GetPosition(null);
_mouseDownResultViewModel = ((FrameworkElement)e.OriginalSource).DataContext as ResultViewModel;
}
private void SuggestionsList_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && _mouseDownResultViewModel?.Result?.ContextData is IFileDropResult fileDropResult)
{
Vector dragDistance = _mouseDownPosition - e.GetPosition(null);
if (Math.Abs(dragDistance.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(dragDistance.Y) > SystemParameters.MinimumVerticalDragDistance)
{
_viewModel.Hide();
try
{
// DoDragDrop with file thumbnail as drag image
var dataObject = DragDataObject.FromFile(fileDropResult.Path);
using var bitmap = DragDataObject.BitmapSourceToBitmap((BitmapSource)_mouseDownResultViewModel?.Image);
IntPtr hBitmap = bitmap.GetHbitmap();
try
{
dataObject.SetDragImage(hBitmap, Constant.ThumbnailSize, Constant.ThumbnailSize);
DragDrop.DoDragDrop(ListBox.SuggestionsList, dataObject, DragDropEffects.Copy);
}
finally
{
Image.NativeMethods.DeleteObject(hBitmap);
}
}
catch
{
// DoDragDrop without drag image
IDataObject dataObject = new DataObject(DataFormats.FileDrop, new[] { fileDropResult.Path });
DragDrop.DoDragDrop(ListBox.SuggestionsList, dataObject, DragDropEffects.Copy);
}
}
}
}
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility))
{
if (Visibility == System.Windows.Visibility.Visible && _viewModel.MainWindowVisibility != Visibility.Hidden)
{
// Not called on first launch
// Called when window is made visible by hotkey. Not called when the window is deactivated by clicking away
UpdatePosition();
BringProcessToForeground();
_viewModel.SetPluginsOverviewVisibility();
_viewModel.SetFontSize();
if (_viewModel.Plugins.Count > 0)
{
_viewModel.SelectedPlugin = null;
pluginsHintsList.ScrollIntoView(pluginsHintsList.Items[0]);
}
// HACK: Setting focus here again fixes some focus issues, like on first run or after showing a message box.
SearchBox.QueryTextBox.Focus();
Keyboard.Focus(SearchBox.QueryTextBox);
if (!_viewModel.LastQuerySelected)
{
_viewModel.LastQuerySelected = true;
}
}
}
else if (e.PropertyName == nameof(MainViewModel.SystemQueryText))
{
_isTextSetProgrammatically = true;
if (_viewModel.Results != null)
{
string newText = MainViewModel.GetSearchText(
_viewModel.Results.SelectedIndex,
_viewModel.SystemQueryText,
_viewModel.QueryText);
if (SearchBox.QueryTextBox.Text != newText)
{
SearchBox.QueryTextBox.Text = newText;
}
else
{
_isTextSetProgrammatically = false;
}
}
}
}
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
DragMove();
}
}
private void InitializePosition()
{
MoveToDesiredPosition();
_settings.WindowTop = Top;
_settings.WindowLeft = Left;
}
private void OnDeactivated(object sender, EventArgs e)
{
if (_settings.HideWhenDeactivated)
{
_viewModel.Hide();
}
}
private void UpdatePosition()
{
if (_settings.RememberLastLaunchLocation)
{
Left = _settings.WindowLeft;
Top = _settings.WindowTop;
}
else
{
MoveToDesiredPosition();
}
}
private void MoveToDesiredPosition()
{
// Hack: After switching to PerMonitorV2, this operation seems to require a three-step operation
// to ensure a stable position: First move to top-left of desired screen, then centralize twice.
// More straightforward ways of doing this don't seem to work well for unclear reasons, but possibly related to
// https://github.com/dotnet/wpf/issues/4127
// In any case, there does not seem to be any big practical downside to doing it this way. As a bonus, it can be
// done in pure WPF without any native calls and without too much DPI-based fiddling.
// In terms of the hack itself, removing any of these three steps seems to fail in certain scenarios only,
// so be careful with testing!
var desiredScreen = GetScreen();
var workingArea = desiredScreen.WorkingArea;
Point ToDIP(double unitX, double unitY) => WindowsInteropHelper.TransformPixelsToDIP(this, unitX, unitY);
// Move to top-left of desired screen.
Top = workingArea.Top;
Left = workingArea.Left;
// Centralize twice.
void MoveToScreenTopCenter()
{
Left = ((ToDIP(workingArea.Width, 0).X - ActualWidth) / 2) + ToDIP(workingArea.X, 0).X;
Top = ((ToDIP(0, workingArea.Height).Y - SearchBox.ActualHeight) / 4) + ToDIP(0, workingArea.Y).Y;
}
MoveToScreenTopCenter();
MoveToScreenTopCenter();
}
private void OnLocationChanged(object sender, EventArgs e)
{
if (_settings != null && _settings.RememberLastLaunchLocation)
{
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
}
}
[PT Run] Run dialog now has monitor positioning options (#9492) * Run dialog now has monitor positioning options * add monitor index validation in window position calculation * correct path in page * change how radio buttons are declared to resolve them not being set based on setting * Change "follow mouse" wording Co-authored-by: htcfreek <61519853+htcfreek@users.noreply.github.com> * PowerLauncher -> PowerToysRun for new variables/resources * correct header label id and update wording to PowerToys Run * only enable custom index if BOTH custom position radio checked and Run is enabled * retrieve list count of detected monitors to limit selection of MonitorToDisplayOn * add a link to Windows Display settings * fix display settings link * change how we get the number of connected monitors so we're not relying on presentation core, windowsbase etc which seem to fail the build * combine position and appearance headers * change references for custom position to "focus" * restore accidentally removed files * remove unused directives * hook up "active window" position with the launcher window * remove left overs * remove uneeded itemgroup * make resource prefixes consistent; using "Run_" * add etcoreapp to spell check * undo change to file not modified in the end * remove unused checkbox post rebase * remove change to reduce diff size * changes according to review * revert whitespace changes post rebase * revert resources * add changes back * Update src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw Add comment Co-authored-by: Enrico Giordani <enricogior@users.noreply.github.com> * remove unneeded resource string Co-authored-by: htcfreek <61519853+htcfreek@users.noreply.github.com> Co-authored-by: Enrico Giordani <enricogior@users.noreply.github.com>
2021-03-09 17:20:49 +00:00
private Screen GetScreen()
{
ManagedCommon.StartupPosition position = _settings.StartupPosition;
switch (position)
{
case ManagedCommon.StartupPosition.PrimaryMonitor:
return Screen.PrimaryScreen;
case ManagedCommon.StartupPosition.Focus:
IntPtr foregroundWindowHandle = NativeMethods.GetForegroundWindow();
Screen activeScreen = Screen.FromHandle(foregroundWindowHandle);
return activeScreen;
case ManagedCommon.StartupPosition.Cursor:
default:
return Screen.FromPoint(System.Windows.Forms.Cursor.Position);
}
}
private void Launcher_KeyDown(object sender, KeyEventArgs e)
{
if (_viewModel.PluginsOverviewVisibility == Visibility.Visible)
{
if (e.Key == Key.Up)
{
_viewModel.SelectPrevOverviewPluginCommand.Execute(null);
pluginsHintsList.ScrollIntoView(_viewModel.SelectedPlugin);
e.Handled = true;
}
else if (e.Key == Key.Down)
{
_viewModel.SelectNextOverviewPluginCommand.Execute(null);
pluginsHintsList.ScrollIntoView(_viewModel.SelectedPlugin);
e.Handled = true;
}
else if (e.Key == Key.Tab && (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)))
{
_viewModel.SelectPrevOverviewPluginCommand.Execute(null);
pluginsHintsList.ScrollIntoView(_viewModel.SelectedPlugin);
e.Handled = true;
}
else if (e.Key == Key.Tab)
{
_viewModel.SelectNextOverviewPluginCommand.Execute(null);
pluginsHintsList.ScrollIntoView(_viewModel.SelectedPlugin);
e.Handled = true;
}
else if (e.Key == Key.Enter)
{
QueryForSelectedPlugin();
e.Handled = true;
}
}
else
{
if (e.Key == Key.Tab && (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)))
{
_viewModel.SelectPrevTabItemCommand.Execute(null);
UpdateTextBoxToSelectedItem();
e.Handled = true;
}
else if (e.Key == Key.Tab)
{
_viewModel.SelectNextTabItemCommand.Execute(null);
UpdateTextBoxToSelectedItem();
e.Handled = true;
}
else if (e.Key == Key.Down)
{
_viewModel.SelectNextItemCommand.Execute(null);
UpdateTextBoxToSelectedItem();
e.Handled = true;
}
else if (e.Key == Key.Up)
{
_viewModel.SelectPrevItemCommand.Execute(null);
UpdateTextBoxToSelectedItem();
e.Handled = true;
}
else if (e.Key == Key.Right)
{
if (SearchBox.QueryTextBox.CaretIndex == SearchBox.QueryTextBox.Text.Length)
{
_viewModel.SelectNextContextMenuItemCommand.Execute(null);
e.Handled = true;
}
}
else if (e.Key == Key.Left)
{
if (SearchBox.QueryTextBox.CaretIndex == SearchBox.QueryTextBox.Text.Length)
{
if (_viewModel.Results != null && _viewModel.Results.IsContextMenuItemSelected())
{
_viewModel.SelectPreviousContextMenuItemCommand.Execute(null);
e.Handled = true;
}
}
}
else if (e.Key == Key.PageDown)
{
_viewModel.SelectNextPageCommand.Execute(null);
e.Handled = true;
}
else if (e.Key == Key.PageUp)
{
_viewModel.SelectPrevPageCommand.Execute(null);
e.Handled = true;
}
else if (e.Key == Key.Back)
{
_deletePressed = true;
}
else
{
_viewModel.HandleContextMenu(e.Key, Keyboard.Modifiers);
}
}
}
private void UpdateTextBoxToSelectedItem()
{
var itemText = _viewModel?.Results?.SelectedItem?.SearchBoxDisplayText() ?? null;
if (!string.IsNullOrEmpty(itemText))
{
_viewModel.ChangeQueryText(itemText);
}
}
private void SuggestionsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListView listview = (ListView)sender;
_viewModel.Results.SelectedItem = (ResultViewModel)listview.SelectedItem;
if (e.AddedItems.Count > 0 && e.AddedItems[0] != null)
{
try
{
listview.ScrollIntoView(e.AddedItems[0]);
}
catch (ArgumentOutOfRangeException ex)
{
// Due to virtualization being enabled for the listview, the layout system updates elements in a deferred manner using an algorithm that balances performance and concurrency.
// Hence, there can be a situation where the element index that we want to scroll into view is out of range for its parent control.
// To mitigate this we use the UpdateLayout function, which forces layout update to ensure that the parent element contains the latest properties.
// However, it has a performance impact and is therefore not called each time.
Log.Exception("The parent element layout is not updated yet", ex, GetType());
listview.UpdateLayout();
listview.ScrollIntoView(e.AddedItems[0]);
}
}
// To populate the AutoCompleteTextBox as soon as the selection is changed or set.
// Setting it here instead of when the text is changed as there is a delay in executing the query and populating the result
if (!string.IsNullOrEmpty(SearchBox.QueryTextBox.Text))
{
SearchBox.AutoCompleteTextBlock.Text = MainViewModel.GetAutoCompleteText(
_viewModel.Results.SelectedIndex,
_viewModel.Results.SelectedItem?.SearchBoxDisplayText(),
_viewModel.QueryText);
}
}
private void QueryTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var textBox = (TextBox)sender;
ClearAutoCompleteText(textBox, null);
PerformSearchQuery(textBox);
}
private void ClearAutoCompleteText(TextBox textBox, System.Reactive.EventPattern<TextChangedEventWithInitiatorArgs> @event)
{
bool isTextSetProgrammaticallyAtStart = _isTextSetProgrammatically;
if (@event != null)
{
@event.EventArgs.IsTextSetProgrammatically = isTextSetProgrammaticallyAtStart;
}
var text = textBox.Text;
var autoCompleteText = SearchBox.AutoCompleteTextBlock.Text;
if (MainViewModel.ShouldAutoCompleteTextBeEmpty(text, autoCompleteText))
{
SearchBox.AutoCompleteTextBlock.Text = string.Empty;
}
var showResultsWithDelay = _viewModel.GetSearchQueryResultsWithDelaySetting();
// only if we are using throttled search and throttled 'fast' search, do we need to do anything different with the current results.
if (showResultsWithDelay && _settings.PTRSearchQueryFastResultsWithDelay)
{
// Default means we don't do anything we did not do before... leave the results as is, they will be changed as needed when results are returned
var pTRunStartNewSearchAction = _settings.PTRunStartNewSearchAction ?? "Default";
if (pTRunStartNewSearchAction == "DeSelect")
{
// leave the results, be deselect anything to it will not be activated by <enter> key, can still be arrow-key or clicked though
if (!isTextSetProgrammaticallyAtStart)
{
DeselectAllResults();
}
}
else if (pTRunStartNewSearchAction == "Clear")
{
// remove all results to prepare for new results, this causes flashing usually and is not cool
if (!isTextSetProgrammaticallyAtStart)
{
ClearResults();
}
}
}
}
private void ClearResults()
{
MainViewModel.PerformSafeAction(() =>
{
_viewModel.Results.SelectedItem = null;
System.Threading.Tasks.Task.Run(() =>
{
Application.Current.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() =>
{
_viewModel.Results.Clear();
_viewModel.Results.Results.NotifyChanges();
}));
});
});
}
private void DeselectAllResults()
{
Application.Current.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() =>
{
_viewModel.Results.SelectedIndex = -1;
}));
}
private void PerformSearchQuery(TextBox textBox)
{
PerformSearchQuery(textBox, null, null);
}
private void PerformSearchQuery(TextBox textBox, bool? delayedExecution, System.Reactive.EventPattern<TextChangedEventWithInitiatorArgs> @event)
{
var text = textBox.Text;
bool isTextSetProgrammaticallyForEvent = _isTextSetProgrammatically;
if (@event != null)
{
isTextSetProgrammaticallyForEvent = @event.EventArgs.IsTextSetProgrammatically;
}
if (isTextSetProgrammaticallyForEvent)
{
textBox.SelectionStart = textBox.Text.Length;
// because IF this is delayedExecution = false (run fast queries) we know this will be called again with delayedExecution = true
// if we don't do this, the second (partner) call will not be called _isTextSetProgrammatically = true also, and we need it to.
// Also, if search query delay is disabled, second call won't come, so reset _isTextSetProgrammatically anyway
if ((delayedExecution.HasValue && delayedExecution.Value) || !_viewModel.GetSearchQueryResultsWithDelaySetting())
{
_isTextSetProgrammatically = false;
}
}
else
{
_viewModel.QueryText = text;
_viewModel.Query(delayedExecution);
}
}
private void ListBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Right)
{
e.Handled = true;
}
}
private void OnVisibilityChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (Visibility == Visibility.Visible)
{
_deletePressed = false;
if (_firstDeleteTimer != null)
{
_firstDeleteTimer.Start();
}
// (this.FindResource("IntroStoryboard") as Storyboard).Begin();
SearchBox.QueryTextBox.Focus();
Keyboard.Focus(SearchBox.QueryTextBox);
_settings.ActivateTimes++;
if (!string.IsNullOrEmpty(SearchBox.QueryTextBox.Text))
{
SearchBox.QueryTextBox.SelectAll();
}
// Log the time taken from pressing the hotkey till launcher is visible as separate events depending on if it's the first hotkey invoke or second
if (!_coldStateHotkeyPressed)
{
PowerToysTelemetry.Log.WriteEvent(new LauncherColdStateHotkeyEvent() { HotkeyToVisibleTimeMs = _viewModel.GetHotkeyEventTimeMs() });
_coldStateHotkeyPressed = true;
}
else
{
PowerToysTelemetry.Log.WriteEvent(new LauncherWarmStateHotkeyEvent() { HotkeyToVisibleTimeMs = _viewModel.GetHotkeyEventTimeMs() });
}
}
else
{
if (_firstDeleteTimer != null)
{
_firstDeleteTimer.Stop();
}
}
}
private void SearchBox_UpdateFlowDirection()
{
SearchBox.QueryTextBox.FlowDirection = MainViewModel.GetLanguageFlowDirection();
SearchBox.AutoCompleteTextBlock.FlowDirection = MainViewModel.GetLanguageFlowDirection();
}
private void SearchBox_InputLanguageChanged(object sender, InputLanguageEventArgs e)
{
SearchBox_UpdateFlowDirection();
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_firstDeleteTimer?.Dispose();
_hwndSource?.Dispose();
}
_firstDeleteTimer = null;
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void OnClosed(object sender, EventArgs e)
{
try
{
_hwndSource.RemoveHook(ProcessWindowMessages);
}
catch (Exception ex)
{
Log.Exception($"Exception when trying to Remove hook", ex, ex.GetType());
}
_hwndSource = null;
}
private void PluginsHintsList_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
QueryForSelectedPlugin();
}
private void QueryForSelectedPlugin()
{
if (_viewModel.Plugins.Count > 0 && _viewModel.SelectedPlugin != null)
{
_viewModel.ChangeQueryText(_viewModel.SelectedPlugin.Metadata.ActionKeyword, true);
SearchBox.QueryTextBox.Focus();
_viewModel.SelectedPlugin = null;
pluginsHintsList.ScrollIntoView(pluginsHintsList.Items[0]);
}
}
}
}