Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Added support for opening all tagged items from the Tags widget #12972

Merged
merged 9 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/Files.App/Actions/Content/Tags/OpenAllTaggedActions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

namespace Files.App.Actions
{
sealed class OpenAllTaggedActions: ObservableObject, IAction
{
private readonly IContentPageContext _pageContext;

private readonly ITagsContext _tagsContext;

public string Label
=> "OpenAllItems".GetLocalizedResource();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label should be OpenAllTaggedItems

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this as well but then I realized this string might be useful outside of tags.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I revert it then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine either way


public string Description
=> "OpenAllItemsDescription".GetLocalizedResource();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAllTaggedItemsDescription


public RichGlyph Glyph
=> new("\uE71D");

public bool IsExecutable =>
_pageContext.ShellPage is not null &&
_tagsContext.TaggedItems.Any();

public OpenAllTaggedActions()
{
_pageContext = Ioc.Default.GetRequiredService<IContentPageContext>();
_tagsContext = Ioc.Default.GetRequiredService<ITagsContext>();

_pageContext.PropertyChanged += Context_PropertyChanged;
_tagsContext.PropertyChanged += Context_PropertyChanged;
}

public async Task ExecuteAsync()
{
var files = _tagsContext.TaggedItems.Where(item => !item.IsFolder);
var folders = _tagsContext.TaggedItems.Where(item => item.IsFolder);

await Task.WhenAll(files.Select(file
=> NavigationHelpers.OpenPath(file.Path, _pageContext.ShellPage!)));

folders.ForEach(async folder => await NavigationHelpers.OpenPathInNewTab(folder.Path));
}

private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(IContentPageContext.ShellPage):
case nameof(ITagsContext.TaggedItems):
OnPropertyChanged(nameof(IsExecutable));
break;
}
}
}
}
1 change: 1 addition & 0 deletions src/Files.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ private IHost ConfigureHost()
.AddSingleton<IDisplayPageContext, DisplayPageContext>()
.AddSingleton<IWindowContext, WindowContext>()
.AddSingleton<IMultitaskingContext, MultitaskingContext>()
.AddSingleton<ITagsContext, TagsContext>()
.AddSingleton<IDialogService, DialogService>()
.AddSingleton<IImageService, ImagingService>()
.AddSingleton<IThreadingService, ThreadingService>()
Expand Down
3 changes: 3 additions & 0 deletions src/Files.App/Data/Commands/CommandCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,8 @@ public enum CommandCodes
GitPull,
GitPush,
GitSync,

// Tags
OpenAllTaggedItems,
}
}
2 changes: 2 additions & 0 deletions src/Files.App/Data/Commands/Manager/CommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ public IRichCommand this[HotKey hotKey]
public IRichCommand GitPull => commands[CommandCodes.GitPull];
public IRichCommand GitPush => commands[CommandCodes.GitPush];
public IRichCommand GitSync => commands[CommandCodes.GitSync];
public IRichCommand OpenAllTaggedItems => commands[CommandCodes.OpenAllTaggedItems];

public CommandManager()
{
Expand Down Expand Up @@ -326,6 +327,7 @@ public CommandManager()
[CommandCodes.GitPull] = new GitPullAction(),
[CommandCodes.GitPush] = new GitPushAction(),
[CommandCodes.GitSync] = new GitSyncAction(),
[CommandCodes.OpenAllTaggedItems] = new OpenAllTaggedActions(),
};

private void UpdateHotKeys()
Expand Down
2 changes: 2 additions & 0 deletions src/Files.App/Data/Commands/Manager/ICommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,7 @@ public interface ICommandManager : IEnumerable<IRichCommand>
IRichCommand GitPull { get; }
IRichCommand GitPush { get; }
IRichCommand GitSync { get; }

IRichCommand OpenAllTaggedItems { get; }
}
}
12 changes: 12 additions & 0 deletions src/Files.App/Data/Contexts/Tags/ITagsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Files.App.ViewModels.Widgets;

namespace Files.App.Data.Contexts
{
interface ITagsContext: INotifyPropertyChanged
{
IEnumerable<FileTagsItemViewModel> TaggedItems { get; }
}
}
40 changes: 40 additions & 0 deletions src/Files.App/Data/Contexts/Tags/TagsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Files.App.ViewModels.Widgets;
using System.Collections.Immutable;

namespace Files.App.Data.Contexts
{
sealed class TagsContext : ITagsContext
{
private static readonly IReadOnlyList<FileTagsItemViewModel> _emptyTaggedItemsList
= Enumerable.Empty<FileTagsItemViewModel>().ToImmutableList();

public event PropertyChangedEventHandler? PropertyChanged;

private IEnumerable<FileTagsItemViewModel> _TaggedItems = _emptyTaggedItemsList;
public IEnumerable<FileTagsItemViewModel> TaggedItems
{
get => _TaggedItems;
set
{
_TaggedItems = value is not null
? value
: _emptyTaggedItemsList;

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TaggedItems)));
}
}

public TagsContext()
{
FileTagsContainerViewModel.SelectedTagsChanged += FileTagsContainerViewModel_SelectedTagsChanged;
}

private void FileTagsContainerViewModel_SelectedTagsChanged(object sender, IEnumerable<FileTagsItemViewModel> items)
{
TaggedItems = items;
}
}
}
6 changes: 6 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3398,4 +3398,10 @@
<data name="OpenInNewWindowDescription" xml:space="preserve">
<value>Open directory in new window</value>
</data>
<data name="OpenAllItems" xml:space="preserve">
<value>Open all</value>
</data>
<data name="OpenAllItemsDescription" xml:space="preserve">
<value>Open all tagged items</value>
</data>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAllTaggedItems and OpenAllTaggedItemsDescription

</root>
46 changes: 26 additions & 20 deletions src/Files.App/UserControls/Widgets/FileTagsWidget.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Files.App.Helpers"
xmlns:local="using:Files.App.UserControls.Widgets"
xmlns:localcontrols="using:Files.App.UserControls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Files.App.ViewModels.Widgets"
d:DesignHeight="300"
Expand Down Expand Up @@ -51,39 +52,44 @@
</Grid.RowDefinitions>

<!-- Title -->
<Grid BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="0,0,0,1">
<Grid
Background="Transparent"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<StackPanel
Padding="12,8,12,8"
Orientation="Horizontal"
Spacing="8">
<!-- Tag Color -->
<PathIcon Data="{StaticResource ColorIconFilledTag}" Foreground="{x:Bind Color, Mode=OneWay, Converter={StaticResource StringToBrushConverter}}" />

<!-- Tag Name -->
<TextBlock
Margin="0,-2,0,0"
FontWeight="SemiBold"
Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>

<!-- View More -->
<HyperlinkButton
Grid.Column="2"
Margin="4"
AutomationProperties.Name="{helpers:ResourceString Name=ViewMore}"
Command="{x:Bind ViewMoreCommand}"
ToolTipService.ToolTip="{helpers:ResourceString Name=ViewMore}">
<HyperlinkButton.Content>
<FontIcon
FontSize="12"
<StackPanel Orientation="Horizontal" Spacing="8">
<!-- Tag Color -->
<PathIcon Data="{StaticResource ColorIconFilledTag}" Foreground="{x:Bind Color, Mode=OneWay, Converter={StaticResource StringToBrushConverter}}" />

<!-- Tag Name -->
<TextBlock
Margin="0,-2,0,0"
FontWeight="SemiBold"
Glyph="&#xE76C;" />
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</HyperlinkButton>

<!-- Open All Items -->
<HyperlinkButton
Grid.Column="2"
Margin="4"
AutomationProperties.Name="{helpers:ResourceString Name=OpenAllItems}"
Command="{x:Bind OpenAllCommand}"
ToolTipService.ToolTip="{helpers:ResourceString Name=OpenAllItems}">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the resources strings are indeed renamed as proposed, these entries will need to be renamed as well.

<HyperlinkButton.Content>
<localcontrols:OpacityIcon Style="{StaticResource ColorIconOpen}" />
</HyperlinkButton.Content>
</HyperlinkButton>
</Grid>
Expand Down
40 changes: 30 additions & 10 deletions src/Files.App/UserControls/Widgets/FileTagsWidget.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ namespace Files.App.UserControls.Widgets
{
public sealed partial class FileTagsWidget : HomePageWidget, IWidgetItemModel
{
private readonly IUserSettingsService userSettingsService;

public FileTagsWidgetViewModel ViewModel
{
get => (FileTagsWidgetViewModel)DataContext;
set => DataContext = value;
}

private readonly IUserSettingsService userSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();

public IShellPage AppInstance;

public Func<string, Task>? OpenAction { get; set; }

public delegate void FileTagsOpenLocationInvokedEventHandler(object sender, PathNavigationEventArgs e);
Expand All @@ -50,6 +51,8 @@ public FileTagsWidgetViewModel ViewModel

public FileTagsWidget()
{
userSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();

InitializeComponent();

// Second function is layered on top to ensure that OpenPath function is late initialized and a null reference is not passed-in
Expand All @@ -67,13 +70,13 @@ public FileTagsWidget()
private void OpenProperties(WidgetCardItem? item)
{
EventHandler<object> flyoutClosed = null!;
flyoutClosed = async (s, e) =>
flyoutClosed = (s, e) =>
{
ItemContextMenuFlyout.Closed -= flyoutClosed;
ListedItem listedItem = new(null!)
{
ItemPath = (item.Item as FileTagsItemViewModel).Path,
ItemNameRaw = (item.Item as FileTagsItemViewModel).Name,
ItemPath = (item.Item as FileTagsItemViewModel)?.Path ?? string.Empty,
ItemNameRaw = (item.Item as FileTagsItemViewModel)?.Name ?? string.Empty,
PrimaryItemAttribute = StorageItemTypes.Folder,
ItemType = "Folder".GetLocalizedResource(),
};
Expand All @@ -96,14 +99,30 @@ private async void FileTagItem_ItemClick(object sender, ItemClickEventArgs e)
await itemViewModel.ClickCommand.ExecuteAsync(null);
}

private async void AdaptiveGridView_RightTapped(object sender, RightTappedRoutedEventArgs e)
private void AdaptiveGridView_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
if (e.OriginalSource is not FrameworkElement element ||
element.DataContext is not FileTagsItemViewModel item)
{
return;
}

LoadContextMenu(
element,
e,
GetItemMenuItems(item, QuickAccessService.IsItemPinned(item.Path), item.IsFolder),
rightClickedItem: item);
}

private async void LoadContextMenu(
FrameworkElement element,
RightTappedRoutedEventArgs e,
List<ContextMenuFlyoutItemViewModel> menuItems,
FileTagsItemViewModel? rightClickedItem = null)
{
var itemContextMenuFlyout = new CommandBarFlyout { Placement = FlyoutPlacementMode.Full };
itemContextMenuFlyout.Opening += (sender, e) => App.LastOpenedFlyout = sender as CommandBarFlyout;
if (e.OriginalSource is not FrameworkElement element || element.DataContext is not FileTagsItemViewModel item)
return;

var menuItems = GetItemMenuItems(item, QuickAccessService.IsItemPinned(item.Path), item.IsFolder);
var (_, secondaryElements) = ItemModelListToContextFlyoutHelper.GetAppBarItemsFromModel(menuItems);

if (!UserSettingsService.GeneralSettingsService.MoveShellExtensionsToSubMenu)
Expand All @@ -113,8 +132,9 @@ private async void AdaptiveGridView_RightTapped(object sender, RightTappedRouted
secondaryElements.ForEach(i => itemContextMenuFlyout.SecondaryCommands.Add(i));
ItemContextMenuFlyout = itemContextMenuFlyout;
itemContextMenuFlyout.ShowAt(element, new FlyoutShowOptions { Position = e.GetPosition(element) });
if (rightClickedItem is not null)
await ShellContextmenuHelper.LoadShellMenuItems(rightClickedItem.Path, itemContextMenuFlyout, showOpenWithMenu: true, showSendToMenu: true);

await ShellContextmenuHelper.LoadShellMenuItems(item.Path, itemContextMenuFlyout, showOpenWithMenu: true, showSendToMenu: true);
e.Handled = true;
}

Expand Down
27 changes: 23 additions & 4 deletions src/Files.App/ViewModels/Widgets/FileTagsContainerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ namespace Files.App.ViewModels.Widgets
public sealed partial class FileTagsContainerViewModel : ObservableObject, IAsyncInitialize
{
private readonly string _tagUid;

private readonly Func<string, Task> _openAction;

private IFileTagsService FileTagsService { get; } = Ioc.Default.GetRequiredService<IFileTagsService>();
private readonly IFileTagsService _fileTagsService;

private readonly IImageService _imageService;

private readonly ICommandManager _commands;

private IImageService ImageService { get; } = Ioc.Default.GetRequiredService<IImageService>();
public delegate void SelectedTagsChangedEventHandler(object sender, IEnumerable<FileTagsItemViewModel> items);

public static event SelectedTagsChangedEventHandler? SelectedTagsChanged;

public ObservableCollection<FileTagsItemViewModel> Tags { get; }

Expand All @@ -25,6 +32,10 @@ public sealed partial class FileTagsContainerViewModel : ObservableObject, IAsyn

public FileTagsContainerViewModel(string tagUid, Func<string, Task> openAction)
{
_fileTagsService = Ioc.Default.GetRequiredService<IFileTagsService>();
_imageService = Ioc.Default.GetRequiredService<IImageService>();
_commands = Ioc.Default.GetRequiredService<ICommandManager>();

_tagUid = tagUid;
_openAction = openAction;
Tags = new();
Expand All @@ -33,9 +44,9 @@ public FileTagsContainerViewModel(string tagUid, Func<string, Task> openAction)
/// <inheritdoc/>
public async Task InitAsync(CancellationToken cancellationToken = default)
{
await foreach (var item in FileTagsService.GetItemsForTagAsync(_tagUid, cancellationToken))
await foreach (var item in _fileTagsService.GetItemsForTagAsync(_tagUid, cancellationToken))
{
var icon = await ImageService.GetIconAsync(item.Storable, cancellationToken);
var icon = await _imageService.GetIconAsync(item.Storable, cancellationToken);
Tags.Add(new(item.Storable, _openAction, icon));
}
}
Expand All @@ -45,5 +56,13 @@ private Task ViewMore(CancellationToken cancellationToken)
{
return _openAction($"tag:{Name}");
}

[RelayCommand]
private Task OpenAll(CancellationToken cancellationToken)
{
SelectedTagsChanged?.Invoke(this, Tags);

return _commands.OpenAllTaggedItems.ExecuteAsync();
}
}
}