Skip to content

Commit

Permalink
The Remove() method now removes a single hotkey entry by only the key…
Browse files Browse the repository at this point in the history
… combination parameters

- at least as long as it can be uniquely identified.
- It will throw an exception if the parameters identify two or more hotkey entries.
  • Loading branch information
jsakamoto committed Mar 31, 2024
1 parent f469a6f commit f013ed4
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 21 deletions.
143 changes: 143 additions & 0 deletions HotKeys2.Test/HotKeysContextTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.JSInterop;

namespace Toolbelt.Blazor.HotKeys2.Test;

public class HotKeysContextTest
{
[Test]
public void Remove_by_Key_Test()
{
// Given
using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance)
.Add(Code.A, () => { })
.Add(Key.F1, () => { }, description: "Show the help document.") // This entry should be removed even though the description is unmatched.
.Add(ModCode.Shift, Code.A, () => { })
.Add(ModKey.Ctrl | ModKey.Alt, Key.F1, () => { });

// When
hotkeysContext.Remove(Key.F1);

// Then
hotkeysContext.Keys
.Select(hotkey => string.Join("+", hotkey.ToStringKeys()))
.Is("A", "Shift+A", "Ctrl+Alt+F1");
}

[Test]
public void Remove_by_Code_and_Mod_Test()
{
// Given
using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance)
.Add(Code.A, () => { })
.Add(Key.F1, () => { })
.Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.None) // This entry should be removed even though the exclude flag is unmatched.
.Add(ModKey.Ctrl | ModKey.Alt, Key.F1, () => { });

// When
hotkeysContext.Remove(ModCode.Shift, Code.A);

// Then
hotkeysContext.Keys
.Select(hotkey => string.Join("+", hotkey.ToStringKeys()))
.Is("A", "F1", "Ctrl+Alt+F1");
}

[Test]
public void Remove_by_Key_and_Exclude_Test()
{
// Given
using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance)
.Add(Code.A, () => { })
.Add(ModKey.Meta, Key.F1, () => { }, new() { Exclude = Exclude.ContentEditable })
.Add(ModCode.Shift, Code.A, () => { })
.Add(ModKey.Meta, Key.F1, () => { });

// When
hotkeysContext.Remove(ModKey.Meta, Key.F1, exclude: Exclude.ContentEditable);

// Then
hotkeysContext.Keys
.Select(hotkey => string.Join("+", hotkey.ToStringKeys()))
.Is("A", "Shift+A", "Meta+F1");
}

[Test]
public void Remove_by_Code_and_ExcludeSelector_Test()
{
// Given
using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance)
.Add(Code.A, () => { })
.Add(ModKey.Meta, Key.F1, () => { })
.Add(Code.A, () => { }, new() { ExcludeSelector = "[data-no-hotkeys]" })
.Add(ModKey.Meta, Key.F1, () => { });

// When
hotkeysContext.Remove(Code.A, excludeSelector: "[data-no-hotkeys]");

// Then
hotkeysContext.Keys
.Select(hotkey => string.Join("+", hotkey.ToStringKeys()))
.Is("A", "Meta+F1", "Meta+F1");
}

[Test]
public void Remove_by_Key_but_Ambiguous_Exception_Test()
{
// Given
using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance)
.Add(ModCode.Shift, Code.A, () => { })
.Add(Key.F1, () => { }, exclude: Exclude.ContentEditable)
.Add(ModCode.Shift, Code.A, () => { })
.Add(Key.F1, () => { }, exclude: Exclude.InputNonText);

// When
Assert.Throws<ArgumentException>(() => hotkeysContext.Remove(Key.F1));

// Then
hotkeysContext.Keys
.Select(hotkey => string.Join("+", hotkey.ToStringKeys()))
.Is("Shift+A", "F1", "Shift+A", "F1");
}

[Test]
public void Remove_by_Code_but_Ambiguous_Exception_Test()
{
// Given
using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance)
.Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.ContentEditable)
.Add(Key.F1, () => { })
.Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.InputNonText)
.Add(Key.F1, () => { });

// When
Assert.Throws<ArgumentException>(() => hotkeysContext.Remove(ModCode.Shift, Code.A));

// Then
hotkeysContext.Keys
.Select(hotkey => string.Join("+", hotkey.ToStringKeys()))
.Is("Shift+A", "F1", "Shift+A", "F1");
}

[Test]
public void Remove_by_Filter_Test()
{
// Given
using var hotkeysContext = new HotKeysContext(Task.FromResult(default(IJSObjectReference)!), NullLogger.Instance)
.Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.ContentEditable)
.Add(Key.F1, () => { })
.Add(ModCode.Shift, Code.A, () => { }, exclude: Exclude.InputNonText)
.Add(Key.F1, () => { });

// When
hotkeysContext.Remove(entries =>
{
return entries.Where(e => e is HotKeyEntryByCode codeEntry && codeEntry.Code == Code.A);
});

// Then
hotkeysContext.Keys
.Select(hotkey => string.Join("+", hotkey.ToStringKeys()))
.Is("F1", "F1");
}
}
71 changes: 50 additions & 21 deletions HotKeys2/HotKeysContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,14 @@ private void Unregister(HotKeyEntry hotKeyEntry)
});
}

private const string _AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE = "Specified parameters are ambiguous to identify the single hotkey entry that should be removed.";

/// <summary>
/// Remove one or more hotkey entries from this context.
/// Remove a hotkey entriy from this context.<br/>
/// If the <paramref name="key"/> parameter cannot find any hotkey entry, this method will return without exception.<br/>
/// If only one hotkey entry can be identified by the <paramref name="key"/> parameter, it will be removed even if the other parameters are not matched.<br/>
/// If the <paramref name="key"/> parameter identifies two or more hotkey entries, the other parameters are referenced to identify the single hotkey entry to be removed.<br/>
/// If the parameters can not determine a single hotkey entry, the <see cref="ArgumentException"/> exception will be thrown.
/// </summary>
/// <param name="key">The identifier of hotkey.</param>
/// <param name="description">The description of the meaning of this hot key entry.</param>
Expand All @@ -655,8 +661,13 @@ private void Unregister(HotKeyEntry hotKeyEntry)
public HotKeysContext Remove(Key key, string description = "", Exclude exclude = Exclude.Default, string excludeSelector = "") =>
this.Remove(ModKey.None, key, description, exclude, excludeSelector);


/// <summary>
/// Remove one or more hotkey entries from this context.
/// Remove a hotkey entriy from this context.<br/>
/// If the combination of the <paramref name="modifiers"/> and the <paramref name="key"/> parameters cannot find any hotkey entry, this method will return without exception.<br/>
/// If only one hotkey entry can be identified by the combination of the <paramref name="modifiers"/> and the <paramref name="key"/> parameters, it will be removed even if the other parameters are not matched.<br/>
/// If the combination of the <paramref name="modifiers"/> and the <paramref name="key"/> parameters identifies two or more hotkey entries, the other parameters are referenced to identify the single hotkey entry to be removed.<br/>
/// If the parameters can not determine a single hotkey entry, the <see cref="ArgumentException"/> exception will be thrown.
/// </summary>
/// <param name="modifiers">The combination of modifier keys flags.</param>
/// <param name="key">The identifier of hotkey.</param>
Expand All @@ -667,18 +678,25 @@ public HotKeysContext Remove(Key key, string description = "", Exclude exclude =
public HotKeysContext Remove(ModKey modifiers, Key key, string description = "", Exclude exclude = Exclude.Default, string excludeSelector = "")
{
var keyEntry = key.ToString();
return this.Remove(keys => keys
.OfType<HotKeyEntryByKey>()
.Where(
k => k.Modifiers == modifiers &&
k.Key.ToString() == keyEntry &&
k.Description == description &&
k.Exclude == exclude &&
k.ExcludeSelector == excludeSelector));
return this.Remove(keys =>
{
var removeCandidates = keys.OfType<HotKeyEntryByKey>().Where(k => k.Modifiers == modifiers && k.Key.ToString() == keyEntry).ToArray();
if (removeCandidates.Length <= 1) return removeCandidates;
removeCandidates = removeCandidates.Where(k => k.Exclude == exclude && k.ExcludeSelector == excludeSelector).ToArray();
if (removeCandidates.Length == 1) return removeCandidates;
if (removeCandidates.Length == 0) throw new ArgumentException(_AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE);
removeCandidates = removeCandidates.Where(k => k.Description == description).ToArray();
if (removeCandidates.Length == 1) return removeCandidates;
throw new ArgumentException(_AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE);
});
}

/// <summary>
/// Remove one or more hotkey entries from this context.
/// Remove a hotkey entriy from this context.<br/>
/// If the <paramref name="code"/> parameter cannot find any hotkey entry, this method will return without exception.<br/>
/// If only one hotkey entry can be identified by the <paramref name="code"/> parameter, it will be removed even if the other parameters are not matched.<br/>
/// If the <paramref name="code"/> parameter identifies two or more hotkey entries, the other parameters are referenced to identify the single hotkey entry to be removed.<br/>
/// If the parameters can not determine a single hotkey entry, the <see cref="ArgumentException"/> exception will be thrown.
/// </summary>
/// <param name="code">The identifier of hotkey.</param>
/// <param name="description">The description of the meaning of this hot key entry.</param>
Expand All @@ -689,7 +707,11 @@ public HotKeysContext Remove(Code code, string description = "", Exclude exclude
=> this.Remove(ModCode.None, code, description, exclude, excludeSelector);

/// <summary>
/// Remove one or more hotkey entries from this context.
/// Remove a hotkey entriy from this context.<br/>
/// If the combination of the <paramref name="modifiers"/> and the <paramref name="code"/> parameters cannot find any hotkey entry, this method will return without exception.<br/>
/// If only one hotkey entry can be identified by the combination of the <paramref name="modifiers"/> and the <paramref name="code"/> parameters, it will be removed even if the other parameters are not matched.<br/>
/// If the combination of the <paramref name="modifiers"/> and the <paramref name="code"/> parameters identifies two or more hotkey entries, the other parameters are referenced to identify the single hotkey entry to be removed.<br/>
/// If the parameters can not determine a single hotkey entry, the <see cref="ArgumentException"/> exception will be thrown.
/// </summary>
/// <param name="modifiers">The combination of modifier keys flags.</param>
/// <param name="code">The identifier of hotkey.</param>
Expand All @@ -700,17 +722,24 @@ public HotKeysContext Remove(Code code, string description = "", Exclude exclude
public HotKeysContext Remove(ModCode modifiers, Code code, string description = "", Exclude exclude = Exclude.Default, string excludeSelector = "")
{
var keyEntry = code.ToString();
return this.Remove(keys => keys
.OfType<HotKeyEntryByCode>()
.Where(
k => k.Modifiers == modifiers &&
k.Code.ToString() == keyEntry &&
k.Description == description &&
k.ExcludeSelector == excludeSelector &&
k.Exclude == exclude));
return this.Remove(keys =>
{
var removeCandidates = keys.OfType<HotKeyEntryByCode>().Where(k => k.Modifiers == modifiers && k.Code.ToString() == keyEntry).ToArray();
if (removeCandidates.Length <= 1) return removeCandidates;
removeCandidates = removeCandidates.Where(k => k.ExcludeSelector == excludeSelector && k.Exclude == exclude).ToArray();
if (removeCandidates.Length == 1) return removeCandidates;
if (removeCandidates.Length == 0) throw new ArgumentException(_AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE);
removeCandidates = removeCandidates.Where(k => k.Description == description).ToArray();
if (removeCandidates.Length == 1) return removeCandidates;
throw new ArgumentException(_AMBIGUOUS_PARAMETER_EXCEPTION_MESSAGE);
});
}

private HotKeysContext Remove(Func<IEnumerable<HotKeyEntry>, IEnumerable<HotKeyEntry>> filter)
/// <summary>
/// Remove all hotkey entries from this context where the <paramref name="filter"/> function returns <c>true</c>.
/// </summary>
/// <param name="filter"></param>
public HotKeysContext Remove(Func<IEnumerable<HotKeyEntry>, IEnumerable<HotKeyEntry>> filter)
{
var entries = filter.Invoke(this.Keys).ToArray();
foreach (var entry in entries)
Expand Down
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,58 @@ You can also specify the elements that are disabled hotkeys by CSS query selecto
And you can specify the `Exclude.ContentEditable` to register the unavailable hotkey when any "contenteditable" applied elements have focus.


### How to remove hotkeys

You can remove hotkkey entries by calling the `Remove()` method of the `HotKeysContext` object, like this.

```csharp
this.HotKeysContext.Remove(ModCode.Ctrl, Code.A);
```

Please remember that the `Remove` method will remove a hotkey entry identified by the `key`, `code`, and `modifiers` parameters even if other parameters are unmatched by the registered hotkey entry as long as it can identify a single hotkey entry.

```csharp
...
this.HotKeys.CreateContext()
.Add(Code.A, OnKeyDownA, exclude: Exclude.InputNonText | Exclude.TextArea);
...
// The following code will remove the hotkey entry registered by the above code
// even though the "exclude" option is different.
this.HotKeysContext.Remove(Code.A);
```

If the parameters for the `Remove` method can not determine a single hotkey entry, the `ArgumentException` exception will be thrown.


```csharp
...
this.HotKeys.CreateContext()
.Add(Code.A, OnKeyDownAForTextArea, exclude: Exclude.InputNonText | Exclude.InputText)
.Add(Code.A, OnKeyDownAForInputText, exclude: Exclude.InputNonText | Exclude.TextArea);
...
// The following code will throw an ArgumentException exception
// because the "Remove" method can not determine a single hotkey entry.
this.HotKeysContext.Remove(Code.A);
...
// The following code will successfully remove the hotkey entry in the second one.
this.HotKeysContext.Remove(Code.A, exclude: Exclude.InputNonText | Exclude.TextArea);
```

If the `key`, `code`, and `modifires` parameters cannot find any hotkey entry, the `Remove` method will return without exception.

The `HotKeysContext` also provides another `Remove` method overload version that accepts a filter function as an argument to determine which hotkey entries to remove. This method will remove all hotkey entries in which the filter function returns.

```csharp
// The following code will remove all hotkey entries registered by the "Code. A",
// regardless of what modifiers, exclude options, etc.
this.HotKeysContext.Remove(entries =>
{
return entries.Where(e => e is HotKeyEntryByCode codeEntry && codeEntry.Code == Code.A);
});
```



## `Code` vs. `Key` - which way should I use to?

There are two ways to register hotkeys in the `HotKeysContext`.
Expand Down

0 comments on commit f013ed4

Please sign in to comment.