diff --git a/src/modules/cmdpal/Exts/EverythingExtension/Commands/CopyPathCommand.cs b/src/modules/cmdpal/Exts/EverythingExtension/Commands/CopyPathCommand.cs index 5d91e6106f..2b3dc9650b 100644 --- a/src/modules/cmdpal/Exts/EverythingExtension/Commands/CopyPathCommand.cs +++ b/src/modules/cmdpal/Exts/EverythingExtension/Commands/CopyPathCommand.cs @@ -2,9 +2,7 @@ // 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.Diagnostics; using Microsoft.CommandPalette.Extensions.Toolkit; -using Microsoft.UI.Xaml; internal sealed partial class CopyPathCommand : InvokableCommand { diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs index e010d1a9f9..0a56bd21c2 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.CmdPal.Ext.Indexer; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -78,9 +79,25 @@ public partial class BookmarksCommandProvider : CommandProvider new BookmarkPlaceholderPage(bookmark) : new UrlCommand(bookmark); - var listItem = new CommandItem(command); + var listItem = new CommandItem(command) { Icon = command.Icon }; List contextMenu = []; + + // Add commands for folder types + if (command is UrlCommand urlCommand) + { + if (urlCommand.Type == "folder") + { + contextMenu.Add( + new CommandContextItem(new DirectoryPage(urlCommand.Url))); + + contextMenu.Add( + new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url))); + } + + listItem.Subtitle = urlCommand.Url; + } + var edit = new AddBookmarkPage(bookmark.Name, bookmark.Bookmark) { Icon = EditIcon }; edit.AddedCommand += AddNewCommand_AddedCommand; contextMenu.Add(new CommandContextItem(edit)); @@ -107,18 +124,6 @@ public partial class BookmarksCommandProvider : CommandProvider }; contextMenu.Add(delete); - // Add commands for folder types - if (command is UrlCommand urlCommand) - { - if (urlCommand.Type == "folder") - { - contextMenu.Add( - new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url))); - } - - listItem.Subtitle = urlCommand.Url; - } - listItem.MoreCommands = contextMenu.ToArray(); return listItem; diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj index c62727e753..15e909314e 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj @@ -11,5 +11,6 @@ + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs index 490f3b8cd8..478f03ef3a 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/OpenFileCommand.cs @@ -19,7 +19,7 @@ internal sealed partial class OpenFileCommand : InvokableCommand { this._item = item; this.Name = Resources.Indexer_Command_OpenFile; - this.Icon = new IconInfo("\uE8E5"); + this.Icon = Icons.OpenFile; } public override CommandResult Invoke() diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/ShowFileInFolderCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/ShowFileInFolderCommand.cs deleted file mode 100644 index 2a94b77a3c..0000000000 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Commands/ShowFileInFolderCommand.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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.Diagnostics; -using System.IO; -using ManagedCommon; -using Microsoft.CmdPal.Ext.Indexer.Data; -using Microsoft.CmdPal.Ext.Indexer.Properties; -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace Microsoft.CmdPal.Ext.Indexer.Commands; - -internal sealed partial class ShowFileInFolderCommand : InvokableCommand -{ - private readonly IndexerItem _item; - - internal ShowFileInFolderCommand(IndexerItem item) - { - this._item = item; - this.Name = Resources.Indexer_Command_ShowInFolder; - this.Icon = new IconInfo("\uE838"); - } - - public override CommandResult Invoke() - { - if (File.Exists(_item.FullPath)) - { - try - { - var argument = "/select, \"" + _item.FullPath + "\""; - Process.Start("explorer.exe", argument); - } - catch (Exception ex) - { - Logger.LogError("Invoke exception: ", ex); - } - } - - return CommandResult.GoHome(); - } -} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs index f415ea4074..5ca5373d13 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerItem.cs @@ -2,6 +2,8 @@ // 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.IO; + namespace Microsoft.CmdPal.Ext.Indexer.Data; internal sealed class IndexerItem @@ -9,4 +11,17 @@ internal sealed class IndexerItem internal string FullPath { get; init; } internal string FileName { get; init; } + + internal bool IsDirectory() + { + if (!Path.Exists(FullPath)) + { + return false; + } + + var attr = File.GetAttributes(FullPath); + + // detect whether its a directory or file + return (attr & FileAttributes.Directory) == FileAttributes.Directory; + } } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs index 49a889f766..57d399b668 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs @@ -2,28 +2,56 @@ // 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.Collections.Generic; using Microsoft.CmdPal.Ext.Indexer.Commands; +using Microsoft.CmdPal.Ext.Indexer.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.Indexer.Data; internal sealed partial class IndexerListItem : ListItem { - private readonly IndexerItem _indexerItem; + internal string FilePath { get; private set; } - public IndexerListItem(IndexerItem indexerItem) + public IndexerListItem( + IndexerItem indexerItem, + IncludeBrowseCommand browseByDefault = IncludeBrowseCommand.Include) : base(new OpenFileCommand(indexerItem)) { - _indexerItem = indexerItem; + FilePath = indexerItem.FullPath; + Title = indexerItem.FileName; Subtitle = indexerItem.FullPath; + List context = []; + if (indexerItem.IsDirectory()) + { + var directoryPage = new DirectoryPage(indexerItem.FullPath); + if (browseByDefault == IncludeBrowseCommand.AsDefault) + { + // Swap the open file command into the context menu + context.Add(new CommandContextItem(Command)); + Command = directoryPage; + } + else if (browseByDefault == IncludeBrowseCommand.Include) + { + context.Add(new CommandContextItem(directoryPage)); + } + } MoreCommands = [ + ..context, new CommandContextItem(new OpenWithCommand(indexerItem)), - new CommandContextItem(new ShowFileInFolderCommand(indexerItem)), + new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }), new CommandContextItem(new CopyPathCommand(indexerItem)), new CommandContextItem(new OpenInConsoleCommand(indexerItem)), new CommandContextItem(new OpenPropertiesCommand(indexerItem)), ]; } } + +internal enum IncludeBrowseCommand +{ + AsDefault = 0, + Include = 1, + Exclude = 2, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs new file mode 100644 index 0000000000..f32fc5bda9 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryExplorePage.cs @@ -0,0 +1,143 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; + +#nullable enable +namespace Microsoft.CmdPal.Ext.Indexer; + +/// +/// This is almost more of just a sample than anything. +/// This is one singular page for switching. +/// +public sealed partial class DirectoryExplorePage : DynamicListPage +{ + private string _path; + private List? _directoryContents; + private List? _filteredContents; + + public DirectoryExplorePage(string path) + { + _path = path; + Icon = Icons.FileExplorerSegoe; + Name = "Browse"; // TODO:LOC + Title = path; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + if (_directoryContents == null) + { + return; + } + + if (string.IsNullOrEmpty(newSearch)) + { + if (_filteredContents != null) + { + _filteredContents = null; + RaiseItemsChanged(-1); + } + + return; + } + + // Need to break this out the manual way so that the compiler can know + // this is an ExploreListItem + var filteredResults = ListHelpers.FilterList( + _directoryContents, + newSearch, + (s, i) => ListHelpers.ScoreListItem(s, i)); + + if (_filteredContents != null) + { + lock (_filteredContents) + { + ListHelpers.InPlaceUpdateList(_filteredContents, filteredResults); + } + } + else + { + _filteredContents = filteredResults.ToList(); + } + + RaiseItemsChanged(-1); + } + + public override IListItem[] GetItems() + { + if (_filteredContents != null) + { + return _filteredContents.ToArray(); + } + + if (_directoryContents != null) + { + return _directoryContents.ToArray(); + } + + IsLoading = true; + if (!Path.Exists(_path)) + { + EmptyContent = new CommandItem(title: "This file doesn't exist!"); // TODO:LOC + return []; + } + + var attr = File.GetAttributes(_path); + + // detect whether its a directory or file + if ((attr & FileAttributes.Directory) != FileAttributes.Directory) + { + EmptyContent = new CommandItem(title: "This is a file, not a folder!"); // TODO:LOC + return []; + } + + var contents = Directory.EnumerateFileSystemEntries(_path); + _directoryContents = contents + .Select(s => new IndexerItem() { FullPath = s, FileName = Path.GetFileName(s) }) + .Select(i => new ExploreListItem(i)) + .ToList(); + + foreach (var i in _directoryContents) + { + i.PathChangeRequested += HandlePathChangeRequested; + } + + _ = Task.Run(() => + { + foreach (var item in _directoryContents) + { + IconInfo? icon = null; + var stream = ThumbnailHelper.GetThumbnail(item.FilePath).Result; + if (stream != null) + { + var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); + icon = new IconInfo(data, data); + item.Icon = icon; + } + } + }); + + IsLoading = false; + + return _directoryContents.ToArray(); + } + + private void HandlePathChangeRequested(ExploreListItem sender, string path) + { + _directoryContents = null; + _filteredContents = null; + _path = path; + Title = path; + SearchText = string.Empty; + RaiseItemsChanged(-1); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs new file mode 100644 index 0000000000..20d9075936 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs @@ -0,0 +1,55 @@ +// 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.Collections.Generic; +using Microsoft.CmdPal.Ext.Indexer.Commands; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +#nullable enable +namespace Microsoft.CmdPal.Ext.Indexer; + +/// +/// This is almost more of just a sample than anything. +/// +internal sealed partial class ExploreListItem : ListItem +{ + internal string FilePath { get; private set; } + + internal event TypedEventHandler? PathChangeRequested; + + public ExploreListItem(IndexerItem indexerItem) + : base(new NoOpCommand()) + { + FilePath = indexerItem.FullPath; + + Title = indexerItem.FileName; + Subtitle = indexerItem.FullPath; + List context = []; + if (indexerItem.IsDirectory()) + { + Command = new AnonymousCommand( + () => { PathChangeRequested?.Invoke(this, FilePath); }) + { + Result = CommandResult.KeepOpen(), + Name = "Browse", // TODO:LOC + }; + context.Add(new CommandContextItem(new DirectoryPage(indexerItem.FullPath))); + } + else + { + Command = new OpenFileCommand(indexerItem); + } + + MoreCommands = [ + ..context, + new CommandContextItem(new OpenWithCommand(indexerItem)), + new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath)), // TODO:LOC, like IndexerListItem + new CommandContextItem(new CopyPathCommand(indexerItem)), + new CommandContextItem(new OpenInConsoleCommand(indexerItem)), + new CommandContextItem(new OpenPropertiesCommand(indexerItem)), + ]; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs index 4b21cf25be..2579f57cc9 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Threading.Tasks; using ManagedCommon; using Microsoft.CmdPal.Ext.Indexer.Data; @@ -27,7 +29,7 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable public IndexerPage() { Id = "com.microsoft.indexer.fileSearch"; - Icon = new IconInfo("\uEC50"); + Icon = Icons.FileExplorerSegoe; Name = Resources.Indexer_Title; PlaceholderText = Resources.Indexer_PlaceholderText; diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs index 040206e60c..e0b5d5a4e2 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Pages/RegistryListPage.cs @@ -16,11 +16,20 @@ internal sealed partial class RegistryListPage : DynamicListPage { public static IconInfo RegistryIcon { get; } = new("\uE74C"); // OEM + private readonly CommandItem _emptyMessage; + public RegistryListPage() { Icon = RegistryIcon; Name = "Windows Registry"; Id = "com.microsoft.cmdpal.registry"; + _emptyMessage = new CommandItem() + { + Icon = RegistryIcon, + Title = "Registry key not found", // TODO:LOC + Subtitle = SearchText, + }; + EmptyContent = _emptyMessage; } public List Query(string query) @@ -58,7 +67,11 @@ internal sealed partial class RegistryListPage : DynamicListPage return []; } - public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(0); + public override void UpdateSearchText(string oldSearch, string newSearch) + { + _emptyMessage.Subtitle = newSearch; + RaiseItemsChanged(0); + } public override IListItem[] GetItems() => Query(SearchText).ToArray(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs index a2adf570ec..8233c39b5b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -120,12 +120,18 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa { _itemTitle = Name, Subtitle = Subtitle, - _listItemIcon = _listItemIcon, - Command = new(model.Command, PageContext), + Command = Command, // TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever }; + // Only set the icon on the context item for us if our command didn't + // have its own icon + if (!Command.HasIcon) + { + _defaultCommandContextItem._listItemIcon = _listItemIcon; + } + model.PropChanged += Model_PropChanged; Command.PropertyChanged += Command_PropertyChanged; UpdateProperty(nameof(Name)); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs index c0e0032010..fdf3d1fba7 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs @@ -33,6 +33,11 @@ public partial class ListViewModel : PageViewModel public event TypedEventHandler? ItemsUpdated; + public bool ShowEmptyContent => + IsInitialized && + FilteredItems.Count == 0 && + IsLoading == false; + // Remember - "observable" properties from the model (via PropChanged) // cannot be marked [ObservableProperty] public bool ShowDetails { get; private set; } @@ -43,12 +48,24 @@ public partial class ListViewModel : PageViewModel public string SearchText { get; private set; } = string.Empty; + public CommandItemViewModel EmptyContent { get; private set; } + private bool _isDynamic; + public override bool IsInitialized + { + get => base.IsInitialized; protected set + { + base.IsInitialized = value; + UpdateEmptyContent(); + } + } + public ListViewModel(IListPage model, TaskScheduler scheduler, CommandPaletteHost host) : base(model, scheduler, host) { _model = new(model); + EmptyContent = new(new(null), PageContext); } // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching? @@ -155,6 +172,8 @@ public partial class ListViewModel : PageViewModel // FilteredItems. The extension already did any filtering it cared about. ListHelpers.InPlaceUpdateList(FilteredItems, Items); } + + UpdateEmptyContent(); } ItemsUpdated?.Invoke(this, EventArgs.Empty); @@ -205,16 +224,38 @@ public partial class ListViewModel : PageViewModel } // InvokeItemCommand is what this will be in Xaml due to source generator + // This is what gets invoked when the user presses [RelayCommand] - private void InvokeItem(ListItemViewModel item) => - WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model)); - - [RelayCommand] - private void InvokeSecondaryCommand(ListItemViewModel item) + private void InvokeItem(ListItemViewModel? item) { - if (item.SecondaryCommand != null) + if (item != null) { - WeakReferenceMessenger.Default.Send(new(item.SecondaryCommand.Command.Model, item.Model)); + WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model)); + } + else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe != null) + { + WeakReferenceMessenger.Default.Send(new( + EmptyContent.PrimaryCommand.Command.Model, + EmptyContent.PrimaryCommand.Model)); + } + } + + // This is what gets invoked when the user presses + [RelayCommand] + private void InvokeSecondaryCommand(ListItemViewModel? item) + { + if (item != null) + { + if (item.SecondaryCommand != null) + { + WeakReferenceMessenger.Default.Send(new(item.SecondaryCommand.Command.Model, item.Model)); + } + } + else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null) + { + WeakReferenceMessenger.Default.Send(new( + EmptyContent.SecondaryCommand.Command.Model, + EmptyContent.SecondaryCommand.Model)); } } @@ -250,25 +291,28 @@ public partial class ListViewModel : PageViewModel { base.InitializeProperties(); - var listPage = _model.Unsafe; - if (listPage == null) + var model = _model.Unsafe; + if (model == null) { return; // throw? } - _isDynamic = listPage is IDynamicListPage; + _isDynamic = model is IDynamicListPage; - ShowDetails = listPage.ShowDetails; + ShowDetails = model.ShowDetails; UpdateProperty(nameof(ShowDetails)); - ModelPlaceholderText = listPage.PlaceholderText; + ModelPlaceholderText = model.PlaceholderText; UpdateProperty(nameof(PlaceholderText)); - SearchText = listPage.SearchText; + SearchText = model.SearchText; UpdateProperty(nameof(SearchText)); + EmptyContent = new(new(model.EmptyContent), PageContext); + EmptyContent.InitializeProperties(); + FetchItems(); - listPage.ItemsChanged += Model_ItemsChanged; + model.ItemsChanged += Model_ItemsChanged; } public void LoadMoreIfNeeded() @@ -300,8 +344,33 @@ public partial class ListViewModel : PageViewModel case nameof(SearchText): this.SearchText = model.SearchText; break; + case nameof(EmptyContent): + EmptyContent = new(new(model.EmptyContent), PageContext); + EmptyContent.InitializeProperties(); + break; + case nameof(IsLoading): + UpdateEmptyContent(); + break; } UpdateProperty(propertyName); } + + private void UpdateEmptyContent() + { + UpdateProperty(nameof(ShowEmptyContent)); + if (!ShowEmptyContent || EmptyContent.Model.Unsafe == null) + { + return; + } + + Task.Factory.StartNew( + () => + { + WeakReferenceMessenger.Default.Send(new(EmptyContent)); + }, + CancellationToken.None, + TaskCreationOptions.None, + PageContext.Scheduler); + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs index 2332ea6c69..ac0b40e14c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs @@ -20,7 +20,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsLoading))] - public partial bool IsInitialized { get; protected set; } + public virtual partial bool IsInitialized { get; protected set; } [ObservableProperty] public partial string ErrorMessage { get; protected set; } = string.Empty; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs index 4091d4668d..a6cb0489cf 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandItemWrapper.cs @@ -180,8 +180,12 @@ public partial class TopLevelCommandItemWrapper : ListItem var listIcon = model.Icon; Icon = model.Icon; break; - - // TODO! MoreCommands array, which needs to also raise HasMoreCommands + case nameof(MoreCommands): + this.MoreCommands = model.MoreCommands; + break; + case nameof(Command): + this.Command = model.Command; + break; } } catch diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index 3fc6a6e0b2..1ed9e50ac5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -226,6 +226,7 @@ public partial class TopLevelCommandManager : ObservableObject, private async Task StartExtensionsAndGetCommands(IEnumerable extensions) { + // TODO This most definitely needs a lock foreach (var extension in extensions) { try diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs index 5ae1a4da6a..e01d4b747c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs @@ -93,7 +93,11 @@ public sealed partial class ContentFormControl : UserControl { _renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard); ContentGrid.Children.Clear(); - ContentGrid.Children.Add(_renderedCard.FrameworkElement); + if (_renderedCard.FrameworkElement != null) + { + ContentGrid.Children.Add(_renderedCard.FrameworkElement); + } + _renderedCard.Action += Rendered_Action; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml index 6aad0802ca..9dbfe718b0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml @@ -96,19 +96,26 @@ - - - - - - + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs index 66a149dbc6..9d736bd943 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs @@ -192,7 +192,11 @@ public sealed partial class ListPage : Page, public void Receive(ActivateSelectedListItemMessage message) { - if (ItemsList.SelectedItem is ListItemViewModel item) + if (ViewModel?.ShowEmptyContent ?? false) + { + ViewModel?.InvokeItemCommand.Execute(null); + } + else if (ItemsList.SelectedItem is ListItemViewModel item) { ViewModel?.InvokeItemCommand.Execute(item); } @@ -200,7 +204,11 @@ public sealed partial class ListPage : Page, public void Receive(ActivateSecondaryCommandMessage message) { - if (ItemsList.SelectedItem is ListItemViewModel item) + if (ViewModel?.ShowEmptyContent ?? false) + { + ViewModel?.InvokeSecondaryCommandCommand.Execute(null); + } + else if (ItemsList.SelectedItem is ListItemViewModel item) { ViewModel?.InvokeSecondaryCommandCommand.Execute(item); } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShowFileInFolderCommand.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShowFileInFolderCommand.cs new file mode 100644 index 0000000000..4180b34cee --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShowFileInFolderCommand.cs @@ -0,0 +1,39 @@ +// 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.Diagnostics; + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public partial class ShowFileInFolderCommand : InvokableCommand +{ + private readonly string _path; + private static readonly IconInfo Ico = new("\uE838"); + + public CommandResult Result { get; set; } = CommandResult.Dismiss(); + + public ShowFileInFolderCommand(string path) + { + _path = path; + Name = "Show in folder"; + Icon = Ico; + } + + public override CommandResult Invoke() + { + if (Path.Exists(_path)) + { + try + { + var argument = "/select, \"" + _path + "\""; + Process.Start("explorer.exe", argument); + } + catch (Exception) + { + } + } + + return Result; + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs new file mode 100644 index 0000000000..745f7c701a --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs @@ -0,0 +1,46 @@ +// 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.IO; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; + +namespace Microsoft.CmdPal.Ext.Indexer; + +internal sealed partial class FallbackOpenFileItem : FallbackCommandItem +{ + public FallbackOpenFileItem() + : base(new NoOpCommand()) + { + Title = string.Empty; + Subtitle = string.Empty; + } + + public override void UpdateQuery(string query) + { + if (Path.Exists(query)) + { + var item = new IndexerItem() { FullPath = query, FileName = Path.GetFileName(query) }; + var listItemForUs = new IndexerListItem(item, IncludeBrowseCommand.AsDefault); + Command = listItemForUs.Command; + MoreCommands = listItemForUs.MoreCommands; + Subtitle = item.FileName; + Title = item.FullPath; + Icon = listItemForUs.Icon; + + var stream = ThumbnailHelper.GetThumbnail(item.FullPath).Result; + if (stream != null) + { + var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); + Icon = new IconInfo(data, data); + } + } + else + { + Title = string.Empty; + Subtitle = string.Empty; + } + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs index 1981369a12..d6cad4ebe8 100644 --- a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs @@ -10,6 +10,8 @@ namespace Microsoft.CmdPal.Ext.Indexer; public partial class IndexerCommandsProvider : CommandProvider { + private readonly FallbackOpenFileItem _fallbackFileItem = new(); + public IndexerCommandsProvider() { DisplayName = Resources.IndexerCommandsProvider_DisplayName; @@ -25,4 +27,9 @@ public partial class IndexerCommandsProvider : CommandProvider } ]; } + + public override IFallbackCommandItem[] FallbackCommands() => + [ + _fallbackFileItem + ]; } diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryPage.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryPage.cs new file mode 100644 index 0000000000..2ab2bd50e9 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Pages/DirectoryPage.cs @@ -0,0 +1,97 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Indexer.Data; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; + +#nullable enable +namespace Microsoft.CmdPal.Ext.Indexer; + +public sealed partial class DirectoryPage : ListPage +{ + private readonly string _path; + + private List? _directoryContents; + + public DirectoryPage(string path) + { + _path = path; + Icon = Icons.FileExplorerSegoe; + Name = "Browse"; // TODO:LOC + Title = path; + } + + public override IListItem[] GetItems() + { + if (_directoryContents != null) + { + return _directoryContents.ToArray(); + } + + if (!Path.Exists(_path)) + { + EmptyContent = new CommandItem( + title: "This path doesn't exist", + subtitle: $"{_path}"); // TODO:LOC + return []; + } + + var attr = File.GetAttributes(_path); + + // detect whether its a directory or file + if ((attr & FileAttributes.Directory) != FileAttributes.Directory) + { + EmptyContent = new CommandItem( + title: "This is a file, not a folder", subtitle: $"{_path}") // TODO:LOC + { + Icon = Icons.Document, + }; + return []; + } + + var contents = Directory.EnumerateFileSystemEntries(_path); + + if (!contents.Any()) + { + var item = new IndexerItem() { FullPath = _path, FileName = Path.GetFileName(_path) }; + var listItemForUs = new IndexerListItem(item, IncludeBrowseCommand.Exclude); + EmptyContent = new CommandItem( + title: "This folder is empty", subtitle: $"{_path}") // TODO:LOC + { + Icon = Icons.FolderOpen, + Command = listItemForUs.Command, + MoreCommands = listItemForUs.MoreCommands, + }; + return []; + } + + _directoryContents = contents + .Select(s => new IndexerItem() { FullPath = s, FileName = Path.GetFileName(s) }) + .Select(i => new IndexerListItem(i, IncludeBrowseCommand.AsDefault)) + .ToList(); + + _ = Task.Run(() => + { + foreach (var item in _directoryContents) + { + IconInfo? icon = null; + var stream = ThumbnailHelper.GetThumbnail(item.FilePath).Result; + if (stream != null) + { + var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); + icon = new IconInfo(data, data); + item.Icon = icon; + } + } + }); + + return _directoryContents.ToArray(); + } +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Pages/Icons.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Pages/Icons.cs new file mode 100644 index 0000000000..376805d8d7 --- /dev/null +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Indexer/Pages/Icons.cs @@ -0,0 +1,18 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Indexer; + +internal sealed class Icons +{ + internal static IconInfo FileExplorerSegoe { get; } = new("\uEC50"); + + internal static IconInfo OpenFile { get; } = new("\uE8E5"); // OpenFile + + internal static IconInfo Document { get; } = new("\uE8A5"); // Document + + internal static IconInfo FolderOpen { get; } = new("\uE838"); // FolderOpen +} diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs index 942c294d04..8d2889e275 100644 --- a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs +++ b/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -51,6 +50,8 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable IListItem[] items = []; lock (_resultsLock) { + // emptySearchForTag === + // we don't have results yet, we haven't typed anything, and we're searching for a tag var emptySearchForTag = _results == null && string.IsNullOrEmpty(SearchText) && HasTag; @@ -62,17 +63,20 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable return items; } - items = (_results == null || !_results.Any()) - ? [ - new ListItem(new NoOpCommand()) - { - Title = (string.IsNullOrEmpty(SearchText) && !HasTag) ? + if (_results != null && _results.Any()) + { + IsLoading = false; + return _results.Select(PackageToListItem).ToArray(); + } + } + + EmptyContent = new CommandItem(new NoOpCommand()) + { + Icon = WinGetIcon, + Title = (string.IsNullOrEmpty(SearchText) && !HasTag) ? "Start typing to search for packages" : "No packages found", - } - ] - : _results.Select(PackageToListItem).ToArray(); - } + }; IsLoading = false;