2020-08-15 04:35:06 +08:00
// 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-20 01:03:12 +08:00
using System.Linq ;
2021-09-09 01:39:51 +08:00
using System.Runtime.InteropServices ;
2020-08-15 04:35:06 +08:00
using System.Timers ;
using System.Windows ;
using System.Windows.Controls ;
using System.Windows.Input ;
2021-08-18 23:43:24 +08:00
using System.Windows.Interop ;
2021-03-20 01:03:12 +08:00
using interop ;
2020-08-15 04:35:06 +08:00
using Microsoft.PowerLauncher.Telemetry ;
using Microsoft.PowerToys.Telemetry ;
using PowerLauncher.Helper ;
2021-03-20 01:03:12 +08:00
using PowerLauncher.Plugin ;
using PowerLauncher.Telemetry.Events ;
2020-08-15 04:35:06 +08:00
using PowerLauncher.ViewModel ;
using Wox.Infrastructure.UserSettings ;
using KeyEventArgs = System . Windows . Input . KeyEventArgs ;
2020-10-24 04:06:22 +08:00
using Log = Wox . Plugin . Logger . Log ;
2020-08-15 04:35:06 +08:00
using Screen = System . Windows . Forms . Screen ;
namespace PowerLauncher
{
public partial class MainWindow : IDisposable
{
2020-10-30 08:52:35 +08:00
private readonly PowerToysRunSettings _settings ;
2020-08-15 04:35:06 +08:00
private readonly MainViewModel _viewModel ;
private bool _isTextSetProgrammatically ;
2020-08-22 03:40:31 +08:00
private bool _deletePressed ;
2021-08-18 23:43:24 +08:00
private HwndSource _hwndSource ;
2020-08-15 04:35:06 +08:00
private Timer _firstDeleteTimer = new Timer ( ) ;
2020-08-22 03:40:31 +08:00
private bool _coldStateHotkeyPressed ;
2021-08-18 23:43:24 +08:00
private bool _disposedValue ;
2020-08-15 04:35:06 +08:00
2020-10-30 08:52:35 +08:00
public MainWindow ( PowerToysRunSettings settings , MainViewModel mainVM )
2020-08-15 04:35:06 +08:00
: this ( )
{
DataContext = mainVM ;
_viewModel = mainVM ;
_settings = settings ;
InitializeComponent ( ) ;
_firstDeleteTimer . Elapsed + = CheckForFirstDelete ;
_firstDeleteTimer . Interval = 1000 ;
2021-03-20 01:03:12 +08:00
NativeEventWaiter . WaitForEventLoop ( Constants . RunSendSettingsTelemetryEvent ( ) , SendSettingsTelemetry ) ;
}
private void SendSettingsTelemetry ( )
{
Log . Info ( "Send Run settings telemetry" , this . GetType ( ) ) ;
var plugins = PluginManager . AllPlugins . ToDictionary ( x = > x . Metadata . Name , x = > new PluginModel ( )
{
Disabled = x . Metadata . Disabled ,
ActionKeyword = x . Metadata . ActionKeyword ,
IsGlobal = x . Metadata . IsGlobal ,
} ) ;
var telemetryEvent = new RunPluginsSettingsEvent ( plugins ) ;
PowerToysTelemetry . Log . WriteEvent ( telemetryEvent ) ;
2020-08-15 04:35:06 +08:00
}
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 ( ) ;
}
2021-09-09 01:39:51 +08:00
private const string EnvironmentChangeType = "Environment" ;
#pragma warning disable CA1801 // Review unused parameters
public IntPtr ProcessWindowMessages ( IntPtr hwnd , int msg , IntPtr wparam , IntPtr lparam , ref bool handled )
#pragma warning restore CA1801 // Review unused parameters
{
switch ( ( WM ) msg )
{
case WM . SETTINGCHANGE :
string changeType = Marshal . PtrToStringUni ( lparam ) ;
if ( changeType = = EnvironmentChangeType )
{
Log . Info ( "Reload environment" , typeof ( EnvironmentHelper ) ) ;
EnvironmentHelper . UpdateEnvironment ( ) ;
handled = true ;
}
break ;
case WM . HOTKEY :
handled = _viewModel . ProcessHotKeyMessages ( wparam , lparam ) ;
break ;
}
return IntPtr . Zero ;
}
2021-08-18 23:43:24 +08:00
private void OnSourceInitialized ( object sender , EventArgs e )
{
_hwndSource = HwndSource . FromHwnd ( new WindowInteropHelper ( this ) . Handle ) ;
2021-09-09 01:39:51 +08:00
_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 ) ;
2021-08-18 23:43:24 +08:00
}
2020-08-15 04:35:06 +08:00
private void OnLoaded ( object sender , RoutedEventArgs e )
{
WindowsInteropHelper . DisableControlBox ( this ) ;
InitializePosition ( ) ;
SearchBox . QueryTextBox . DataContext = _viewModel ;
SearchBox . QueryTextBox . PreviewKeyDown + = Launcher_KeyDown ;
SearchBox . QueryTextBox . TextChanged + = QueryTextBox_TextChanged ;
// Set initial language flow direction
SearchBox_UpdateFlowDirection ( ) ;
// Register language changed event
InputLanguageManager . Current . InputLanguageChanged + = SearchBox_InputLanguageChanged ;
SearchBox . QueryTextBox . Focus ( ) ;
2020-09-03 04:34:07 +08:00
SearchBox . QueryTextBox . ControlledElements . Add ( ListBox . SuggestionsList ) ;
2020-08-15 04:35:06 +08:00
ListBox . DataContext = _viewModel ;
ListBox . SuggestionsList . SelectionChanged + = SuggestionsList_SelectionChanged ;
ListBox . SuggestionsList . PreviewMouseLeftButtonUp + = SuggestionsList_PreviewMouseLeftButtonUp ;
_viewModel . PropertyChanged + = ViewModel_PropertyChanged ;
2021-07-15 16:51:41 +08:00
_viewModel . MainWindowVisibility = Visibility . Collapsed ;
2021-08-18 18:20:48 +08:00
_viewModel . LoadedAtLeastOnce = true ;
2020-08-15 04:35:06 +08:00
BringProcessToForeground ( ) ;
}
private void SuggestionsList_PreviewMouseLeftButtonUp ( object sender , MouseButtonEventArgs e )
{
var result = ( ( FrameworkElement ) e . OriginalSource ) . DataContext ;
if ( result ! = null )
{
// 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-15 04:12:02 +08:00
_viewModel . OpenResultWithMouseCommand . Execute ( null ) ;
2020-08-15 04:35:06 +08:00
}
}
}
private void ViewModel_PropertyChanged ( object sender , PropertyChangedEventArgs e )
{
if ( e . PropertyName = = nameof ( MainViewModel . MainWindowVisibility ) )
{
2020-09-15 04:10:56 +08:00
if ( Visibility = = System . Windows . Visibility . Visible & & _viewModel . MainWindowVisibility ! = Visibility . Hidden )
2020-08-15 04:35:06 +08:00
{
// Not called on first launch
2020-09-15 04:10:56 +08:00
// Called when window is made visible by hotkey. Not called when the window is deactivated by clicking away
2020-08-15 04:35:06 +08:00
UpdatePosition ( ) ;
BringProcessToForeground ( ) ;
if ( ! _viewModel . LastQuerySelected )
{
_viewModel . LastQuerySelected = true ;
}
}
}
else if ( e . PropertyName = = nameof ( MainViewModel . SystemQueryText ) )
{
_isTextSetProgrammatically = true ;
if ( _viewModel . Results ! = null )
{
SearchBox . QueryTextBox . Text = MainViewModel . GetSearchText (
_viewModel . Results . SelectedIndex ,
_viewModel . SystemQueryText ,
_viewModel . QueryText ) ;
}
}
}
private void OnMouseDown ( object sender , MouseButtonEventArgs e )
{
if ( e . ChangedButton = = MouseButton . Left )
{
DragMove ( ) ;
}
}
private void InitializePosition ( )
{
Top = WindowTop ( ) ;
Left = WindowLeft ( ) ;
_settings . WindowTop = Top ;
_settings . WindowLeft = Left ;
}
private void OnDeactivated ( object sender , EventArgs e )
{
if ( _settings . HideWhenDeactivated )
{
// (this.FindResource("OutroStoryboard") as Storyboard).Begin();
2021-03-19 02:04:04 +08:00
_viewModel . Hide ( ) ;
2020-08-15 04:35:06 +08:00
}
}
private void UpdatePosition ( )
{
if ( _settings . RememberLastLaunchLocation )
{
Left = _settings . WindowLeft ;
Top = _settings . WindowTop ;
}
else
{
Top = WindowTop ( ) ;
Left = WindowLeft ( ) ;
}
}
private void OnLocationChanged ( object sender , EventArgs e )
{
if ( _settings . RememberLastLaunchLocation )
{
_settings . WindowLeft = Left ;
_settings . WindowTop = Top ;
}
}
/// <summary>
/// Calculates X co-ordinate of main window top left corner.
/// </summary>
/// <returns>X co-ordinate of main window top left corner</returns>
private double WindowLeft ( )
{
2021-03-10 01:20:49 +08:00
var screen = GetScreen ( ) ;
2020-08-15 04:35:06 +08:00
var dip1 = WindowsInteropHelper . TransformPixelsToDIP ( this , screen . WorkingArea . X , 0 ) ;
var dip2 = WindowsInteropHelper . TransformPixelsToDIP ( this , screen . WorkingArea . Width , 0 ) ;
var left = ( ( dip2 . X - ActualWidth ) / 2 ) + dip1 . X ;
return left ;
}
private double WindowTop ( )
{
2021-03-10 01:20:49 +08:00
var screen = GetScreen ( ) ;
2020-08-15 04:35:06 +08:00
var dip1 = WindowsInteropHelper . TransformPixelsToDIP ( this , 0 , screen . WorkingArea . Y ) ;
var dip2 = WindowsInteropHelper . TransformPixelsToDIP ( this , 0 , screen . WorkingArea . Height ) ;
var top = ( ( dip2 . Y - SearchBox . ActualHeight ) / 4 ) + dip1 . Y ;
return top ;
}
2021-03-10 01:20:49 +08: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 ) ;
}
}
2020-08-15 04:35:06 +08:00
private void Launcher_KeyDown ( object sender , KeyEventArgs e )
{
if ( e . Key = = Key . Tab & & Keyboard . IsKeyDown ( Key . LeftShift ) )
{
_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 ( )
{
2020-09-03 04:34:07 +08:00
var itemText = _viewModel ? . Results ? . SelectedItem ? . SearchBoxDisplayText ( ) ? ? null ;
2020-08-15 04:35:06 +08:00
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 )
{
2020-09-03 04:39:57 +08:00
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 it's 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.
2020-09-24 07:32:06 +08:00
Log . Exception ( "The parent element layout is not updated yet" , ex , GetType ( ) ) ;
2020-09-03 04:39:57 +08:00
listview . UpdateLayout ( ) ;
listview . ScrollIntoView ( e . AddedItems [ 0 ] ) ;
}
2020-08-15 04:35:06 +08:00
}
// 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 ( _viewModel . Results ! = null )
{
SearchBox . AutoCompleteTextBlock . Text = MainViewModel . GetAutoCompleteText (
_viewModel . Results . SelectedIndex ,
2020-09-03 04:34:07 +08:00
_viewModel . Results . SelectedItem ? . SearchBoxDisplayText ( ) ,
2020-08-15 04:35:06 +08:00
_viewModel . QueryText ) ;
}
}
private void QueryTextBox_TextChanged ( object sender , TextChangedEventArgs e )
{
2020-08-29 05:58:57 +08:00
var textBox = ( TextBox ) sender ;
var text = textBox . Text ;
2020-10-21 05:53:32 +08:00
var autoCompleteText = SearchBox . AutoCompleteTextBlock . Text ;
2020-08-29 05:58:57 +08:00
2020-10-21 05:53:32 +08:00
if ( MainViewModel . ShouldAutoCompleteTextBeEmpty ( text , autoCompleteText ) )
2020-08-29 05:58:57 +08:00
{
SearchBox . AutoCompleteTextBlock . Text = string . Empty ;
}
2020-08-15 04:35:06 +08:00
if ( _isTextSetProgrammatically )
{
textBox . SelectionStart = textBox . Text . Length ;
_isTextSetProgrammatically = false ;
}
else
{
_viewModel . QueryText = text ;
_viewModel . Query ( ) ;
}
}
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 OutroStoryboard_Completed ( object sender , EventArgs e )
{
Hide ( ) ;
}
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 )
{
2021-08-18 23:43:24 +08:00
if ( ! _disposedValue )
2020-08-15 04:35:06 +08:00
{
if ( disposing )
{
if ( _firstDeleteTimer ! = null )
{
_firstDeleteTimer . Dispose ( ) ;
}
2021-08-18 23:43:24 +08:00
_hwndSource ? . Dispose ( ) ;
2020-08-15 04:35:06 +08:00
}
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
_firstDeleteTimer = null ;
2021-08-18 23:43:24 +08:00
_disposedValue = true ;
2020-08-15 04:35:06 +08:00
}
}
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~MainWindow()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose ( )
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose ( disposing : true ) ;
GC . SuppressFinalize ( this ) ;
}
2021-09-09 01:39:51 +08:00
private void OnClosed ( object sender , EventArgs e )
{
_hwndSource . RemoveHook ( ProcessWindowMessages ) ;
_hwndSource = null ;
}
2020-08-15 04:35:06 +08:00
}
}