Skip to content

Commit fd002b5

Browse files
authored
Feature: Support changing behavior of buttons with modifier keys (#12728)
1 parent 9d2b7d4 commit fd002b5

File tree

9 files changed

+235
-60
lines changed

9 files changed

+235
-60
lines changed

src/Files.App/App.xaml.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
204204
.AddSingleton<ICloudDetector, CloudDetector>()
205205
.AddSingleton<IFileTagsService, FileTagsService>()
206206
.AddSingleton<ICommandManager, CommandManager>()
207+
.AddSingleton<IModifiableCommandManager, ModifiableCommandManager>()
207208
#if UWP
208209
.AddSingleton<IStorageService, WindowsStorageService>()
209210
#else

src/Files.App/Commands/Manager/CommandManager.cs

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -362,48 +362,8 @@ private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e
362362
UpdateHotKeys();
363363
}
364364

365-
[DebuggerDisplay("Command None")]
366-
private class NoneCommand : IRichCommand
367-
{
368-
public event EventHandler? CanExecuteChanged { add {} remove {} }
369-
public event PropertyChangingEventHandler? PropertyChanging { add {} remove {} }
370-
public event PropertyChangedEventHandler? PropertyChanged { add {} remove {} }
371-
372-
public CommandCodes Code => CommandCodes.None;
373-
374-
public string Label => string.Empty;
375-
public string LabelWithHotKey => string.Empty;
376-
public string AutomationName => string.Empty;
377-
378-
public string Description => string.Empty;
379-
380-
public RichGlyph Glyph => RichGlyph.None;
381-
public object? Icon => null;
382-
public FontIcon? FontIcon => null;
383-
public Style? OpacityStyle => null;
384-
385-
public bool IsCustomHotKeys => false;
386-
public string? HotKeyText => null;
387-
public HotKeyCollection HotKeys
388-
{
389-
get => HotKeyCollection.Empty;
390-
set => throw new InvalidOperationException("This command is readonly.");
391-
}
392-
393-
public bool IsToggle => false;
394-
public bool IsOn { get => false; set {} }
395-
public bool IsExecutable => false;
396-
397-
public bool CanExecute(object? parameter) => false;
398-
public void Execute(object? parameter) {}
399-
public Task ExecuteAsync() => Task.CompletedTask;
400-
public void ExecuteTapped(object sender, TappedRoutedEventArgs e) {}
401-
402-
public void ResetHotKeys() {}
403-
}
404-
405365
[DebuggerDisplay("Command {Code}")]
406-
private class ActionCommand : ObservableObject, IRichCommand
366+
internal class ActionCommand : ObservableObject, IRichCommand
407367
{
408368
public event EventHandler? CanExecuteChanged;
409369

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
namespace Files.App.Commands
5+
{
6+
public interface IModifiableCommandManager : IEnumerable<IRichCommand>
7+
{
8+
IRichCommand this[CommandCodes code] { get; }
9+
10+
IRichCommand None { get; }
11+
12+
IRichCommand PasteItem { get; }
13+
IRichCommand DeleteItem { get; }
14+
}
15+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using System.Collections.Immutable;
5+
6+
namespace Files.App.Commands
7+
{
8+
internal class ModifiableCommandManager : IModifiableCommandManager
9+
{
10+
private static readonly ICommandManager Commands = Ioc.Default.GetRequiredService<ICommandManager>();
11+
12+
private readonly IImmutableDictionary<CommandCodes, IRichCommand> ModifiableCommands;
13+
14+
public IRichCommand this[CommandCodes code] => ModifiableCommands.TryGetValue(code, out var command) ? command : None;
15+
16+
public IRichCommand None => ModifiableCommands[CommandCodes.None];
17+
public IRichCommand PasteItem => ModifiableCommands[CommandCodes.PasteItem];
18+
public IRichCommand DeleteItem => ModifiableCommands[CommandCodes.DeleteItem];
19+
20+
public ModifiableCommandManager()
21+
{
22+
ModifiableCommands = CreateModifiableCommands().ToImmutableDictionary();
23+
}
24+
25+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
26+
public IEnumerator<IRichCommand> GetEnumerator() => ModifiableCommands.Values.GetEnumerator();
27+
28+
private static IDictionary<CommandCodes, IRichCommand> CreateModifiableCommands() => new Dictionary<CommandCodes, IRichCommand>
29+
{
30+
[CommandCodes.None] = new NoneCommand(),
31+
[CommandCodes.PasteItem] = new ModifiableCommand(Commands.PasteItem, new() {
32+
{ KeyModifiers.Shift, Commands.PasteItemToSelection }
33+
}),
34+
[CommandCodes.DeleteItem] = new ModifiableCommand(Commands.DeleteItem, new() {
35+
{ KeyModifiers.Shift, Commands.DeleteItemPermanently }
36+
}),
37+
};
38+
}
39+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using Files.App.Actions;
5+
using Microsoft.UI.Xaml;
6+
using Microsoft.UI.Xaml.Controls;
7+
using Microsoft.UI.Xaml.Input;
8+
using System.Collections.Immutable;
9+
using static Files.App.Commands.CommandManager;
10+
11+
namespace Files.App.Commands
12+
{
13+
[DebuggerDisplay("Command {Code} (Modifiable)")]
14+
internal class ModifiableCommand : ObservableObject, IRichCommand
15+
{
16+
public event EventHandler? CanExecuteChanged;
17+
18+
private IRichCommand BaseCommand;
19+
private ImmutableDictionary<KeyModifiers, IRichCommand> ModifiedCommands;
20+
21+
public CommandCodes Code => BaseCommand.Code;
22+
23+
public string Label => BaseCommand.Label;
24+
public string LabelWithHotKey => BaseCommand.LabelWithHotKey;
25+
public string AutomationName => BaseCommand.AutomationName;
26+
27+
public string Description => BaseCommand.Description;
28+
29+
public RichGlyph Glyph => BaseCommand.Glyph;
30+
public object? Icon => BaseCommand.Icon;
31+
public FontIcon? FontIcon => BaseCommand.FontIcon;
32+
public Style? OpacityStyle => BaseCommand.OpacityStyle;
33+
34+
public bool IsCustomHotKeys => BaseCommand.IsCustomHotKeys;
35+
public string? HotKeyText => BaseCommand.HotKeyText;
36+
37+
public HotKeyCollection HotKeys
38+
{
39+
get => BaseCommand.HotKeys;
40+
set => BaseCommand.HotKeys = value;
41+
}
42+
43+
public bool IsToggle => BaseCommand.IsToggle;
44+
45+
public bool IsOn
46+
{
47+
get => BaseCommand.IsOn;
48+
set => BaseCommand.IsOn = value;
49+
}
50+
51+
public bool IsExecutable => BaseCommand.IsExecutable;
52+
53+
public ModifiableCommand(IRichCommand baseCommand, Dictionary<KeyModifiers, IRichCommand> modifiedCommands)
54+
{
55+
BaseCommand = baseCommand;
56+
ModifiedCommands = modifiedCommands.ToImmutableDictionary();
57+
58+
if (baseCommand is ActionCommand actionCommand)
59+
{
60+
if (actionCommand.Action is INotifyPropertyChanging notifyPropertyChanging)
61+
notifyPropertyChanging.PropertyChanging += Action_PropertyChanging;
62+
if (actionCommand.Action is INotifyPropertyChanged notifyPropertyChanged)
63+
notifyPropertyChanged.PropertyChanged += Action_PropertyChanged;
64+
}
65+
}
66+
67+
public bool CanExecute(object? parameter) => BaseCommand.CanExecute(parameter);
68+
public async void Execute(object? parameter) => await ExecuteAsync();
69+
70+
public Task ExecuteAsync()
71+
{
72+
if (ModifiedCommands.TryGetValue(HotKeyHelpers.GetCurrentKeyModifiers(), out var modifiedCommand) &&
73+
modifiedCommand.IsExecutable)
74+
return modifiedCommand.ExecuteAsync();
75+
else
76+
return BaseCommand.ExecuteAsync();
77+
}
78+
79+
public async void ExecuteTapped(object sender, TappedRoutedEventArgs e) => await ExecuteAsync();
80+
81+
public void ResetHotKeys() => BaseCommand.ResetHotKeys();
82+
83+
private void Action_PropertyChanging(object? sender, PropertyChangingEventArgs e)
84+
{
85+
switch (e.PropertyName)
86+
{
87+
case nameof(IAction.Label):
88+
OnPropertyChanging(nameof(Label));
89+
OnPropertyChanging(nameof(LabelWithHotKey));
90+
OnPropertyChanging(nameof(AutomationName));
91+
break;
92+
case nameof(IToggleAction.IsOn) when IsToggle:
93+
OnPropertyChanging(nameof(IsOn));
94+
break;
95+
case nameof(IAction.IsExecutable):
96+
OnPropertyChanging(nameof(IsExecutable));
97+
break;
98+
}
99+
}
100+
private void Action_PropertyChanged(object? sender, PropertyChangedEventArgs e)
101+
{
102+
switch (e.PropertyName)
103+
{
104+
case nameof(IAction.Label):
105+
OnPropertyChanged(nameof(Label));
106+
OnPropertyChanged(nameof(LabelWithHotKey));
107+
OnPropertyChanged(nameof(AutomationName));
108+
break;
109+
case nameof(IToggleAction.IsOn) when IsToggle:
110+
OnPropertyChanged(nameof(IsOn));
111+
break;
112+
case nameof(IAction.IsExecutable):
113+
OnPropertyChanged(nameof(IsExecutable));
114+
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
115+
break;
116+
}
117+
}
118+
}
119+
}

src/Files.App/Commands/NoneCommand.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using Microsoft.UI.Xaml;
5+
using Microsoft.UI.Xaml.Controls;
6+
using Microsoft.UI.Xaml.Input;
7+
8+
namespace Files.App.Commands
9+
{
10+
[DebuggerDisplay("Command None")]
11+
internal class NoneCommand : IRichCommand
12+
{
13+
public event EventHandler? CanExecuteChanged { add { } remove { } }
14+
public event PropertyChangingEventHandler? PropertyChanging { add { } remove { } }
15+
public event PropertyChangedEventHandler? PropertyChanged { add { } remove { } }
16+
17+
public CommandCodes Code => CommandCodes.None;
18+
19+
public string Label => string.Empty;
20+
public string LabelWithHotKey => string.Empty;
21+
public string AutomationName => string.Empty;
22+
23+
public string Description => string.Empty;
24+
25+
public RichGlyph Glyph => RichGlyph.None;
26+
public object? Icon => null;
27+
public FontIcon? FontIcon => null;
28+
public Style? OpacityStyle => null;
29+
30+
public bool IsCustomHotKeys => false;
31+
public string? HotKeyText => null;
32+
public HotKeyCollection HotKeys
33+
{
34+
get => HotKeyCollection.Empty;
35+
set => throw new InvalidOperationException("This command is readonly.");
36+
}
37+
38+
public bool IsToggle => false;
39+
public bool IsOn { get => false; set { } }
40+
public bool IsExecutable => false;
41+
42+
public bool CanExecute(object? parameter) => false;
43+
public void Execute(object? parameter) { }
44+
public Task ExecuteAsync() => Task.CompletedTask;
45+
public void ExecuteTapped(object sender, TappedRoutedEventArgs e) { }
46+
47+
public void ResetHotKeys() { }
48+
}
49+
}

src/Files.App/Helpers/ContextFlyoutItemHelper.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
// Licensed under the MIT License. See the LICENSE.
33

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

2624
public static List<ContextMenuFlyoutItemViewModel> GetItemContextCommandsWithoutShellItems(CurrentInstanceViewModel currentInstanceViewModel, List<ListedItem> selectedItems, BaseLayoutCommandsViewModel commandsViewModel, bool shiftPressed, SelectedItemsPropertiesViewModel? selectedItemsPropertiesViewModel, ItemViewModel? itemViewModel = null)
@@ -485,7 +483,7 @@ public static List<ContextMenuFlyoutItemViewModel> GetBaseItemMenuItems(
485483
{
486484
IsPrimary = true
487485
}.Build(),
488-
new ContextMenuFlyoutItemViewModelBuilder(commands.DeleteItem)
486+
new ContextMenuFlyoutItemViewModelBuilder(modifiableCommands.DeleteItem)
489487
{
490488
IsVisible = itemsSelected,
491489
IsPrimary = true,

src/Files.App/UserControls/InnerNavigationToolbar.xaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,11 @@
145145
MinWidth="40"
146146
AccessKey="V"
147147
AutomationProperties.AutomationId="InnerNavigationToolbarPasteButton"
148-
Command="{x:Bind Commands.PasteItem}"
149-
Label="{x:Bind Commands.PasteItem.Label}"
148+
Command="{x:Bind ModifiableCommands.PasteItem}"
149+
Label="{x:Bind ModifiableCommands.PasteItem.Label}"
150150
LabelPosition="Collapsed"
151-
ToolTipService.ToolTip="{x:Bind Commands.PasteItem.LabelWithHotKey, Mode=OneWay}">
152-
<local:OpacityIcon Style="{x:Bind Commands.PasteItem.OpacityStyle}" />
151+
ToolTipService.ToolTip="{x:Bind ModifiableCommands.PasteItem.LabelWithHotKey, Mode=OneWay}">
152+
<local:OpacityIcon Style="{x:Bind ModifiableCommands.PasteItem.OpacityStyle}" />
153153
</AppBarButton>
154154

155155
<!-- Rename -->
@@ -187,11 +187,11 @@
187187
Width="Auto"
188188
MinWidth="40"
189189
AutomationProperties.AutomationId="InnerNavigationToolbarDeleteButton"
190-
Command="{x:Bind Commands.DeleteItem}"
191-
Label="{x:Bind Commands.DeleteItem.Label}"
190+
Command="{x:Bind ModifiableCommands.DeleteItem}"
191+
Label="{x:Bind ModifiableCommands.DeleteItem.Label}"
192192
LabelPosition="Collapsed"
193-
ToolTipService.ToolTip="{x:Bind Commands.DeleteItem.LabelWithHotKey, Mode=OneWay}">
194-
<local:OpacityIcon Style="{x:Bind Commands.DeleteItem.OpacityStyle}" />
193+
ToolTipService.ToolTip="{x:Bind ModifiableCommands.DeleteItem.LabelWithHotKey, Mode=OneWay}">
194+
<local:OpacityIcon Style="{x:Bind ModifiableCommands.DeleteItem.OpacityStyle}" />
195195
</AppBarButton>
196196

197197
<!-- Properties -->

src/Files.App/UserControls/InnerNavigationToolbar.xaml.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
// Copyright (c) 2023 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using CommunityToolkit.Mvvm.DependencyInjection;
54
using Files.App.Commands;
6-
using Files.App.Data.Models;
7-
using Files.App.ViewModels;
8-
using Files.Backend.Services;
9-
using Files.Backend.Services.Settings;
105
using Microsoft.UI.Xaml;
116
using Microsoft.UI.Xaml.Controls;
127
using Microsoft.UI.Xaml.Input;
138
using Microsoft.UI.Xaml.Media.Imaging;
14-
using System;
159
using System.IO;
16-
using System.Linq;
1710

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

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

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

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

0 commit comments

Comments
 (0)