Browse command and EmptyContent (#499)

Due to the nature of FHL I didn't do a great job disentangling the following two big features:

* Support for `EmptyContent`
  * closes #261
* Support for a "Browse" `DirectoryPage` which is like a file explorer. 
  * Folder bookmarks can open it
  * So can folders in the file search
* Also adds a fallback command for if you type a file path, we'll show that


drive-by fixes the icon on the context menu flyout, if the Command has an icon that's different from the list item. We'll prefer the Command's icon to the CommandItem's icon
This commit is contained in:
Mike Griese
2025-03-07 19:55:54 -06:00
committed by GitHub
parent abdd298c3c
commit 5147b26a01
25 changed files with 667 additions and 111 deletions

View File

@@ -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
{

View File

@@ -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<CommandContextItem> 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;

View File

@@ -11,5 +11,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
</ItemGroup>
</Project>

View File

@@ -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()

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<CommandContextItem> 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,
}

View File

@@ -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;
/// <summary>
/// This is almost more of just a sample than anything.
/// This is one singular page for switching.
/// </summary>
public sealed partial class DirectoryExplorePage : DynamicListPage
{
private string _path;
private List<ExploreListItem>? _directoryContents;
private List<ExploreListItem>? _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<ExploreListItem>(_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);
}
}

View File

@@ -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;
/// <summary>
/// This is almost more of just a sample than anything.
/// </summary>
internal sealed partial class ExploreListItem : ListItem
{
internal string FilePath { get; private set; }
internal event TypedEventHandler<ExploreListItem, string>? PathChangeRequested;
public ExploreListItem(IndexerItem indexerItem)
: base(new NoOpCommand())
{
FilePath = indexerItem.FullPath;
Title = indexerItem.FileName;
Subtitle = indexerItem.FullPath;
List<CommandContextItem> 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)),
];
}
}

View File

@@ -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;

View File

@@ -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<ListItem> 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();
}

View File

@@ -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));

View File

@@ -33,6 +33,11 @@ public partial class ListViewModel : PageViewModel
public event TypedEventHandler<ListViewModel, object>? 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 <enter>
[RelayCommand]
private void InvokeItem(ListItemViewModel item) =>
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(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<PerformCommandMessage>(new(item.SecondaryCommand.Command.Model, item.Model));
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
}
else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
EmptyContent.PrimaryCommand.Command.Model,
EmptyContent.PrimaryCommand.Model));
}
}
// This is what gets invoked when the user presses <ctrl+enter>
[RelayCommand]
private void InvokeSecondaryCommand(ListItemViewModel? item)
{
if (item != null)
{
if (item.SecondaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.SecondaryCommand.Command.Model, item.Model));
}
}
else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(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<UpdateCommandBarMessage>(new(EmptyContent));
},
CancellationToken.None,
TaskCreationOptions.None,
PageContext.Scheduler);
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -226,6 +226,7 @@ public partial class TopLevelCommandManager : ObservableObject,
private async Task StartExtensionsAndGetCommands(IEnumerable<IExtensionWrapper> extensions)
{
// TODO This most definitely needs a lock
foreach (var extension in extensions)
{
try

View File

@@ -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;
}

View File

@@ -96,19 +96,26 @@
</DataTemplate>
</Page.Resources>
<ListView
x:Name="ItemsList"
DoubleTapped="ItemsList_DoubleTapped"
IsDoubleTapEnabled="True"
IsItemClickEnabled="True"
ItemClick="ItemsList_ItemClick"
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
SelectionChanged="ItemsList_SelectionChanged">
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
<!--<ListView.GroupStyle>
<Grid>
<controls:SwitchPresenter
HorizontalAlignment="Stretch"
TargetType="x:Boolean"
Value="{x:Bind ViewModel.ShowEmptyContent, Mode=OneWay}">
<controls:Case Value="False">
<ListView
x:Name="ItemsList"
DoubleTapped="ItemsList_DoubleTapped"
IsDoubleTapEnabled="True"
IsItemClickEnabled="True"
ItemClick="ItemsList_ItemClick"
ItemTemplate="{StaticResource ListItemViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
SelectionChanged="ItemsList_SelectionChanged">
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
<!--<ListView.GroupStyle>
<GroupStyle HidesIfEmpty="True">
<GroupStyle.HeaderTemplate>
<DataTemplate>
@@ -120,5 +127,34 @@
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>-->
</ListView>
</ListView>
</controls:Case>
<controls:Case Value="True">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical">
<cpcontrols:IconBox
x:Name="IconBorder"
Grid.Column="0"
Width="64"
Height="64"
Margin="8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
HorizontalAlignment="Center"
FontSize="24"
Style="{ThemeResource HeaderTextBlockStyle}"
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}" />
<TextBlock HorizontalAlignment="Center" Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}" />
</StackPanel>
</controls:Case>
</controls:SwitchPresenter>
</Grid>
</Page>

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
];
}

View File

@@ -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<IndexerListItem>? _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();
}
}

View File

@@ -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
}

View File

@@ -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;