mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
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:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user