diff --git a/src/modules/peek/Peek.UI/MainWindowViewModel.cs b/src/modules/peek/Peek.UI/MainWindowViewModel.cs index db54e346cc..775664c042 100644 --- a/src/modules/peek/Peek.UI/MainWindowViewModel.cs +++ b/src/modules/peek/Peek.UI/MainWindowViewModel.cs @@ -62,6 +62,12 @@ namespace Peek.UI [ObservableProperty] private IFileSystemItem? _currentItem; + /// + /// Work around missing navigation when peeking from CLI. + /// TODO: Implement navigation when peeking from CLI. + /// + private bool _isFromCli; + partial void OnCurrentItemChanged(IFileSystemItem? value) { WindowTitle = value != null @@ -129,7 +135,24 @@ namespace Peek.UI NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs); } - public void Initialize(HWND foregroundWindowHandle) + public void Initialize(SelectedItem selectedItem) + { + switch (selectedItem) + { + case SelectedItemByPath selectedItemByPath: + InitializeFromCli(selectedItemByPath.Path); + break; + + case SelectedItemByWindowHandle selectedItemByWindowHandle: + InitializeFromExplorer(selectedItemByWindowHandle.WindowHandle); + break; + + default: + throw new NotImplementedException($"Invalid type of selected item: '{selectedItem.GetType().FullName}'"); + } + } + + private void InitializeFromExplorer(HWND foregroundWindowHandle) { try { @@ -141,10 +164,20 @@ namespace Peek.UI } _currentIndex = DisplayIndex = 0; + _isFromCli = false; CurrentItem = (Items != null && Items.Count > 0) ? Items[0] : null; } + private void InitializeFromCli(string path) + { + // TODO: implement navigation + _isFromCli = true; + Items = null; + _currentIndex = DisplayIndex = 0; + CurrentItem = new FileItem(path, Path.GetFileName(path)); + } + public void Uninitialize() { _currentIndex = DisplayIndex = 0; @@ -153,6 +186,7 @@ namespace Peek.UI Items = null; _navigationDirection = NavigationDirection.Forwards; IsErrorVisible = false; + _isFromCli = false; } public void AttemptPreviousNavigation() => Navigate(NavigationDirection.Backwards); @@ -166,6 +200,12 @@ namespace Peek.UI return; } + // TODO: implement navigation. + if (_isFromCli) + { + return; + } + if (Items == null || Items.Count == _deletedItemIndexes.Count) { _currentIndex = DisplayIndex = 0; diff --git a/src/modules/peek/Peek.UI/Models/SelectedItem.cs b/src/modules/peek/Peek.UI/Models/SelectedItem.cs new file mode 100644 index 0000000000..9d0cc6568a --- /dev/null +++ b/src/modules/peek/Peek.UI/Models/SelectedItem.cs @@ -0,0 +1,11 @@ +// 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. + +namespace Peek.UI.Models +{ + public abstract class SelectedItem + { + public abstract bool Matches(string? path); + } +} diff --git a/src/modules/peek/Peek.UI/Models/SelectedItemByPath.cs b/src/modules/peek/Peek.UI/Models/SelectedItemByPath.cs new file mode 100644 index 0000000000..5f53865bfd --- /dev/null +++ b/src/modules/peek/Peek.UI/Models/SelectedItemByPath.cs @@ -0,0 +1,23 @@ +// 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; + +namespace Peek.UI.Models +{ + public class SelectedItemByPath : SelectedItem + { + public string Path { get; } + + public SelectedItemByPath(string path) + { + Path = path; + } + + public override bool Matches(string? path) + { + return string.Equals(Path, path, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/modules/peek/Peek.UI/Models/SelectedItemByWindowHandle.cs b/src/modules/peek/Peek.UI/Models/SelectedItemByWindowHandle.cs new file mode 100644 index 0000000000..e93b2f94ca --- /dev/null +++ b/src/modules/peek/Peek.UI/Models/SelectedItemByWindowHandle.cs @@ -0,0 +1,34 @@ +// 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 Peek.UI.Extensions; +using Peek.UI.Helpers; +using Windows.Win32.Foundation; + +namespace Peek.UI.Models +{ + public class SelectedItemByWindowHandle : SelectedItem + { + public HWND WindowHandle { get; } + + public SelectedItemByWindowHandle(HWND windowHandle) + { + WindowHandle = windowHandle; + } + + public override bool Matches(string? path) + { + var selectedItems = FileExplorerHelper.GetSelectedItems(WindowHandle); + var selectedItemsCount = selectedItems?.GetCount() ?? 0; + if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1) + { + return false; + } + + var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path; + var currentItemPath = path; + return fileExplorerSelectedItemPath != null && currentItemPath != null && fileExplorerSelectedItemPath != currentItemPath; + } + } +} diff --git a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs index 9bd66e380f..b89e871a4d 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs @@ -3,16 +3,19 @@ // See the LICENSE file in the project root for more information. using System; - +using System.IO; +using System.Threading; using ManagedCommon; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls.Primitives; using Peek.Common; using Peek.FilePreviewer; using Peek.FilePreviewer.Models; using Peek.FilePreviewer.Previewers; +using Peek.UI.Models; using Peek.UI.Native; using Peek.UI.Telemetry.Events; using Peek.UI.Views; @@ -23,7 +26,7 @@ namespace Peek.UI /// /// Provides application-specific behavior to supplement the default Application class. /// - public partial class App : Application, IApp + public partial class App : Application, IApp, IDisposable { public static int PowerToysPID { get; set; } @@ -36,6 +39,10 @@ namespace Peek.UI private MainWindow? Window { get; set; } + private bool _disposed; + private SelectedItem? _selectedItem; + private bool _launchedFromCli; + /// /// Initializes a new instance of the class. /// Initializes the singleton application object. This is the first line of authored code @@ -52,22 +59,22 @@ namespace Peek.UI InitializeComponent(); Logger.InitializeLogger("\\Peek\\Logs"); - Host = Microsoft.Extensions.Hosting.Host. - CreateDefaultBuilder(). - UseContentRoot(AppContext.BaseDirectory). - ConfigureServices((context, services) => - { - // Core Services - services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); + Host = Microsoft.Extensions.Hosting.Host + .CreateDefaultBuilder() + .UseContentRoot(AppContext.BaseDirectory) + .ConfigureServices((context, services) => + { + // Core Services + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); - // Views and ViewModels - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - }). - Build(); + // Views and ViewModels + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + }) + .Build(); UnhandledException += App_UnhandledException; } @@ -99,6 +106,7 @@ namespace Peek.UI var cmdArgs = Environment.GetCommandLineArgs(); if (cmdArgs?.Length > 1) { + // Check if the last argument is a PowerToys Runner PID if (int.TryParse(cmdArgs[^1], out int powerToysRunnerPid)) { RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () => @@ -107,9 +115,25 @@ namespace Peek.UI Environment.Exit(0); }); } + else + { + // Command line argument is a file path - activate Peek with that file + string filePath = cmdArgs[^1]; + if (File.Exists(filePath) || Directory.Exists(filePath)) + { + _selectedItem = new SelectedItemByPath(filePath); + _launchedFromCli = true; + OnShowPeek(); + return; + } + else + { + Logger.LogError($"Command line argument is not a valid file or directory: {filePath}"); + } + } } - NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey); + NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnShowPeek); NativeEventWaiter.WaitForEventLoop(Constants.TerminatePeekEvent(), () => { ShellPreviewHandlerPreviewer.ReleaseHandlerFactories(); @@ -126,11 +150,16 @@ namespace Peek.UI /// /// Handle Peek hotkey /// - private void OnPeekHotkey() + private void OnShowPeek() { - // Need to read the foreground HWND before activating Peek to avoid focus stealing - // Foreground HWND must always be Explorer or Desktop - var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow(); + // null means explorer, not null means CLI + if (_selectedItem == null) + { + // Need to read the foreground HWND before activating Peek to avoid focus stealing + // Foreground HWND must always be Explorer or Desktop + var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow(); + _selectedItem = new SelectedItemByWindowHandle(foregroundWindowHandle); + } bool firstActivation = false; @@ -140,7 +169,38 @@ namespace Peek.UI Window = new MainWindow(); } - Window.Toggle(firstActivation, foregroundWindowHandle); + Window.Toggle(firstActivation, _selectedItem, _launchedFromCli); + _launchedFromCli = false; + _selectedItem = null; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // dispose managed state (managed objects) + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + _disposed = true; + } + } + + /* // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~App() + // { + // // 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); } } } diff --git a/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs index 4edad9a807..2c8983c634 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs @@ -18,6 +18,7 @@ using Peek.FilePreviewer.Models; using Peek.FilePreviewer.Previewers; using Peek.UI.Extensions; using Peek.UI.Helpers; +using Peek.UI.Models; using Peek.UI.Telemetry.Events; using Windows.Foundation; using WinUIEx; @@ -38,6 +39,7 @@ namespace Peek.UI /// dialog is open at a time. /// private bool _isDeleteInProgress; + private bool _exitAfterClose; public MainWindow() { @@ -116,12 +118,17 @@ namespace Peek.UI /// /// Toggling the window visibility and querying files when necessary. /// - public void Toggle(bool firstActivation, Windows.Win32.Foundation.HWND foregroundWindowHandle) + public void Toggle(bool firstActivation, SelectedItem selectedItem, bool exitAfterClose) { + if (exitAfterClose) + { + _exitAfterClose = true; + } + if (firstActivation) { Activate(); - Initialize(foregroundWindowHandle); + Initialize(selectedItem); return; } @@ -132,9 +139,9 @@ namespace Peek.UI if (AppWindow.IsVisible) { - if (IsNewSingleSelectedItem(foregroundWindowHandle)) + if (IsNewSingleSelectedItem(selectedItem)) { - Initialize(foregroundWindowHandle); + Initialize(selectedItem); Activate(); // Brings existing window into focus in case it was previously minimized } else @@ -144,7 +151,7 @@ namespace Peek.UI } else { - Initialize(foregroundWindowHandle); + Initialize(selectedItem); } } @@ -182,12 +189,12 @@ namespace Peek.UI Uninitialize(); } - private void Initialize(Windows.Win32.Foundation.HWND foregroundWindowHandle) + private void Initialize(SelectedItem selectedItem) { var bootTime = new System.Diagnostics.Stopwatch(); bootTime.Start(); - ViewModel.Initialize(foregroundWindowHandle); + ViewModel.Initialize(selectedItem); ViewModel.ScalingFactor = this.GetMonitorScale(); this.Content.KeyUp += Content_KeyUp; @@ -207,6 +214,11 @@ namespace Peek.UI this.Content.KeyUp -= Content_KeyUp; ShellPreviewHandlerPreviewer.ReleaseHandlerFactories(); + + if (_exitAfterClose) + { + Environment.Exit(0); + } } /// @@ -272,20 +284,11 @@ namespace Peek.UI Uninitialize(); } - private bool IsNewSingleSelectedItem(Windows.Win32.Foundation.HWND foregroundWindowHandle) + private bool IsNewSingleSelectedItem(SelectedItem selectedItem) { try { - var selectedItems = FileExplorerHelper.GetSelectedItems(foregroundWindowHandle); - var selectedItemsCount = selectedItems?.GetCount() ?? 0; - if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1) - { - return false; - } - - var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path; - var currentItemPath = ViewModel.CurrentItem?.Path; - return fileExplorerSelectedItemPath != null && currentItemPath != null && fileExplorerSelectedItemPath != currentItemPath; + return selectedItem.Matches(ViewModel.CurrentItem?.Path); } catch (Exception ex) {