Skip to content

Feature: Support changing behavior of buttons with modifier keys #12728

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

Merged
merged 6 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/Files.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
.AddSingleton<ICloudDetector, CloudDetector>()
.AddSingleton<IFileTagsService, FileTagsService>()
.AddSingleton<ICommandManager, CommandManager>()
.AddSingleton<IModifiableCommandManager, ModifiableCommandManager>()
#if UWP
.AddSingleton<IStorageService, WindowsStorageService>()
#else
Expand Down
42 changes: 1 addition & 41 deletions src/Files.App/Commands/Manager/CommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,48 +362,8 @@ private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e
UpdateHotKeys();
}

[DebuggerDisplay("Command None")]
private class NoneCommand : IRichCommand
{
public event EventHandler? CanExecuteChanged { add {} remove {} }
public event PropertyChangingEventHandler? PropertyChanging { add {} remove {} }
public event PropertyChangedEventHandler? PropertyChanged { add {} remove {} }

public CommandCodes Code => CommandCodes.None;

public string Label => string.Empty;
public string LabelWithHotKey => string.Empty;
public string AutomationName => string.Empty;

public string Description => string.Empty;

public RichGlyph Glyph => RichGlyph.None;
public object? Icon => null;
public FontIcon? FontIcon => null;
public Style? OpacityStyle => null;

public bool IsCustomHotKeys => false;
public string? HotKeyText => null;
public HotKeyCollection HotKeys
{
get => HotKeyCollection.Empty;
set => throw new InvalidOperationException("This command is readonly.");
}

public bool IsToggle => false;
public bool IsOn { get => false; set {} }
public bool IsExecutable => false;

public bool CanExecute(object? parameter) => false;
public void Execute(object? parameter) {}
public Task ExecuteAsync() => Task.CompletedTask;
public void ExecuteTapped(object sender, TappedRoutedEventArgs e) {}

public void ResetHotKeys() {}
}

[DebuggerDisplay("Command {Code}")]
private class ActionCommand : ObservableObject, IRichCommand
internal class ActionCommand : ObservableObject, IRichCommand
{
public event EventHandler? CanExecuteChanged;

Expand Down
15 changes: 15 additions & 0 deletions src/Files.App/Commands/Manager/IModifiableCommandManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

namespace Files.App.Commands
{
public interface IModifiableCommandManager : IEnumerable<IRichCommand>
{
IRichCommand this[CommandCodes code] { get; }

IRichCommand None { get; }

IRichCommand PasteItem { get; }
IRichCommand DeleteItem { get; }
}
}
39 changes: 39 additions & 0 deletions src/Files.App/Commands/Manager/ModifiableCommandManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using System.Collections.Immutable;

namespace Files.App.Commands
{
internal class ModifiableCommandManager : IModifiableCommandManager
{
private static readonly ICommandManager Commands = Ioc.Default.GetRequiredService<ICommandManager>();

private readonly IImmutableDictionary<CommandCodes, IRichCommand> ModifiableCommands;

public IRichCommand this[CommandCodes code] => ModifiableCommands.TryGetValue(code, out var command) ? command : None;

public IRichCommand None => ModifiableCommands[CommandCodes.None];
public IRichCommand PasteItem => ModifiableCommands[CommandCodes.PasteItem];
public IRichCommand DeleteItem => ModifiableCommands[CommandCodes.DeleteItem];

public ModifiableCommandManager()
{
ModifiableCommands = CreateModifiableCommands().ToImmutableDictionary();
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<IRichCommand> GetEnumerator() => ModifiableCommands.Values.GetEnumerator();

private static IDictionary<CommandCodes, IRichCommand> CreateModifiableCommands() => new Dictionary<CommandCodes, IRichCommand>
{
[CommandCodes.None] = new NoneCommand(),
[CommandCodes.PasteItem] = new ModifiableCommand(Commands.PasteItem, new() {
{ KeyModifiers.Shift, Commands.PasteItemToSelection }
}),
[CommandCodes.DeleteItem] = new ModifiableCommand(Commands.DeleteItem, new() {
{ KeyModifiers.Shift, Commands.DeleteItemPermanently }
}),
};
}
}
119 changes: 119 additions & 0 deletions src/Files.App/Commands/ModifiableCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Files.App.Actions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using System.Collections.Immutable;
using static Files.App.Commands.CommandManager;

namespace Files.App.Commands
{
[DebuggerDisplay("Command {Code} (Modifiable)")]
internal class ModifiableCommand : ObservableObject, IRichCommand
{
public event EventHandler? CanExecuteChanged;

private IRichCommand BaseCommand;
private ImmutableDictionary<KeyModifiers, IRichCommand> ModifiedCommands;

public CommandCodes Code => BaseCommand.Code;

public string Label => BaseCommand.Label;
public string LabelWithHotKey => BaseCommand.LabelWithHotKey;
public string AutomationName => BaseCommand.AutomationName;

public string Description => BaseCommand.Description;

public RichGlyph Glyph => BaseCommand.Glyph;
public object? Icon => BaseCommand.Icon;
public FontIcon? FontIcon => BaseCommand.FontIcon;
public Style? OpacityStyle => BaseCommand.OpacityStyle;

public bool IsCustomHotKeys => BaseCommand.IsCustomHotKeys;
public string? HotKeyText => BaseCommand.HotKeyText;

public HotKeyCollection HotKeys
{
get => BaseCommand.HotKeys;
set => BaseCommand.HotKeys = value;
}

public bool IsToggle => BaseCommand.IsToggle;

public bool IsOn
{
get => BaseCommand.IsOn;
set => BaseCommand.IsOn = value;
}

public bool IsExecutable => BaseCommand.IsExecutable;

public ModifiableCommand(IRichCommand baseCommand, Dictionary<KeyModifiers, IRichCommand> modifiedCommands)
{
BaseCommand = baseCommand;
ModifiedCommands = modifiedCommands.ToImmutableDictionary();

if (baseCommand is ActionCommand actionCommand)
{
if (actionCommand.Action is INotifyPropertyChanging notifyPropertyChanging)
notifyPropertyChanging.PropertyChanging += Action_PropertyChanging;
if (actionCommand.Action is INotifyPropertyChanged notifyPropertyChanged)
notifyPropertyChanged.PropertyChanged += Action_PropertyChanged;
}
}

public bool CanExecute(object? parameter) => BaseCommand.CanExecute(parameter);
public async void Execute(object? parameter) => await ExecuteAsync();

public Task ExecuteAsync()
{
if (ModifiedCommands.TryGetValue(HotKeyHelpers.GetCurrentKeyModifiers(), out var modifiedCommand) &&
modifiedCommand.IsExecutable)
return modifiedCommand.ExecuteAsync();
else
return BaseCommand.ExecuteAsync();
}

public async void ExecuteTapped(object sender, TappedRoutedEventArgs e) => await ExecuteAsync();

public void ResetHotKeys() => BaseCommand.ResetHotKeys();

private void Action_PropertyChanging(object? sender, PropertyChangingEventArgs e)
{
switch (e.PropertyName)
{
case nameof(IAction.Label):
OnPropertyChanging(nameof(Label));
OnPropertyChanging(nameof(LabelWithHotKey));
OnPropertyChanging(nameof(AutomationName));
break;
case nameof(IToggleAction.IsOn) when IsToggle:
OnPropertyChanging(nameof(IsOn));
break;
case nameof(IAction.IsExecutable):
OnPropertyChanging(nameof(IsExecutable));
break;
}
}
private void Action_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(IAction.Label):
OnPropertyChanged(nameof(Label));
OnPropertyChanged(nameof(LabelWithHotKey));
OnPropertyChanged(nameof(AutomationName));
break;
case nameof(IToggleAction.IsOn) when IsToggle:
OnPropertyChanged(nameof(IsOn));
break;
case nameof(IAction.IsExecutable):
OnPropertyChanged(nameof(IsExecutable));
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
break;
}
}
}
}
49 changes: 49 additions & 0 deletions src/Files.App/Commands/NoneCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;

namespace Files.App.Commands
{
[DebuggerDisplay("Command None")]
internal class NoneCommand : IRichCommand
{
public event EventHandler? CanExecuteChanged { add { } remove { } }
public event PropertyChangingEventHandler? PropertyChanging { add { } remove { } }
public event PropertyChangedEventHandler? PropertyChanged { add { } remove { } }

public CommandCodes Code => CommandCodes.None;

public string Label => string.Empty;
public string LabelWithHotKey => string.Empty;
public string AutomationName => string.Empty;

public string Description => string.Empty;

public RichGlyph Glyph => RichGlyph.None;
public object? Icon => null;
public FontIcon? FontIcon => null;
public Style? OpacityStyle => null;

public bool IsCustomHotKeys => false;
public string? HotKeyText => null;
public HotKeyCollection HotKeys
{
get => HotKeyCollection.Empty;
set => throw new InvalidOperationException("This command is readonly.");
}

public bool IsToggle => false;
public bool IsOn { get => false; set { } }
public bool IsExecutable => false;

public bool CanExecute(object? parameter) => false;
public void Execute(object? parameter) { }
public Task ExecuteAsync() => Task.CompletedTask;
public void ExecuteTapped(object sender, TappedRoutedEventArgs e) { }

public void ResetHotKeys() { }
}
}
6 changes: 2 additions & 4 deletions src/Files.App/Helpers/ContextFlyoutItemHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
// Licensed under the MIT License. See the LICENSE.

using Files.App.Commands;
using Files.App.Services.Settings;
using Files.Backend.Helpers;
using Files.Backend.Services;
using Microsoft.UI.Xaml.Media.Imaging;
using System.IO;
using Windows.Storage;
Expand All @@ -21,6 +18,7 @@ public static class ContextFlyoutItemHelper
{
private static readonly IUserSettingsService userSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();
private static readonly ICommandManager commands = Ioc.Default.GetRequiredService<ICommandManager>();
private static readonly IModifiableCommandManager modifiableCommands = Ioc.Default.GetRequiredService<IModifiableCommandManager>();
private static readonly IAddItemService addItemService = Ioc.Default.GetRequiredService<IAddItemService>();

public static List<ContextMenuFlyoutItemViewModel> GetItemContextCommandsWithoutShellItems(CurrentInstanceViewModel currentInstanceViewModel, List<ListedItem> selectedItems, BaseLayoutCommandsViewModel commandsViewModel, bool shiftPressed, SelectedItemsPropertiesViewModel? selectedItemsPropertiesViewModel, ItemViewModel? itemViewModel = null)
Expand Down Expand Up @@ -485,7 +483,7 @@ public static List<ContextMenuFlyoutItemViewModel> GetBaseItemMenuItems(
{
IsPrimary = true
}.Build(),
new ContextMenuFlyoutItemViewModelBuilder(commands.DeleteItem)
new ContextMenuFlyoutItemViewModelBuilder(modifiableCommands.DeleteItem)
{
IsVisible = itemsSelected,
IsPrimary = true,
Expand Down
16 changes: 8 additions & 8 deletions src/Files.App/UserControls/InnerNavigationToolbar.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@
MinWidth="40"
AccessKey="V"
AutomationProperties.AutomationId="InnerNavigationToolbarPasteButton"
Command="{x:Bind Commands.PasteItem}"
Label="{x:Bind Commands.PasteItem.Label}"
Command="{x:Bind ModifiableCommands.PasteItem}"
Label="{x:Bind ModifiableCommands.PasteItem.Label}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="{x:Bind Commands.PasteItem.LabelWithHotKey, Mode=OneWay}">
<local:OpacityIcon Style="{x:Bind Commands.PasteItem.OpacityStyle}" />
ToolTipService.ToolTip="{x:Bind ModifiableCommands.PasteItem.LabelWithHotKey, Mode=OneWay}">
<local:OpacityIcon Style="{x:Bind ModifiableCommands.PasteItem.OpacityStyle}" />
</AppBarButton>

<!-- Rename -->
Expand Down Expand Up @@ -187,11 +187,11 @@
Width="Auto"
MinWidth="40"
AutomationProperties.AutomationId="InnerNavigationToolbarDeleteButton"
Command="{x:Bind Commands.DeleteItem}"
Label="{x:Bind Commands.DeleteItem.Label}"
Command="{x:Bind ModifiableCommands.DeleteItem}"
Label="{x:Bind ModifiableCommands.DeleteItem.Label}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="{x:Bind Commands.DeleteItem.LabelWithHotKey, Mode=OneWay}">
<local:OpacityIcon Style="{x:Bind Commands.DeleteItem.OpacityStyle}" />
ToolTipService.ToolTip="{x:Bind ModifiableCommands.DeleteItem.LabelWithHotKey, Mode=OneWay}">
<local:OpacityIcon Style="{x:Bind ModifiableCommands.DeleteItem.OpacityStyle}" />
</AppBarButton>

<!-- Properties -->
Expand Down
8 changes: 1 addition & 7 deletions src/Files.App/UserControls/InnerNavigationToolbar.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using CommunityToolkit.Mvvm.DependencyInjection;
using Files.App.Commands;
using Files.App.Data.Models;
using Files.App.ViewModels;
using Files.Backend.Services;
using Files.Backend.Services.Settings;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.IO;
using System.Linq;

// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236

Expand All @@ -29,6 +22,7 @@ public InnerNavigationToolbar()

public IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService<IUserSettingsService>();
public ICommandManager Commands { get; } = Ioc.Default.GetRequiredService<ICommandManager>();
public IModifiableCommandManager ModifiableCommands { get; } = Ioc.Default.GetRequiredService<IModifiableCommandManager>();

private readonly IAddItemService addItemService = Ioc.Default.GetRequiredService<IAddItemService>();

Expand Down