Skip to content

Commit 1596c59

Browse files
authored
Merge pull request #2667 from Flow-Launcher/duplicate-hotkey-handling
Handle duplicate hotkeys in HotkeyControlDialog
2 parents 4377fd1 + 2cce361 commit 1596c59

File tree

11 files changed

+347
-32
lines changed

11 files changed

+347
-32
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Collections.Generic;
2+
3+
namespace Flow.Launcher.Infrastructure.Hotkey;
4+
5+
/// <summary>
6+
/// Interface that you should implement in your settings class to be able to pass it to
7+
/// <c>Flow.Launcher.HotkeyControlDialog</c>. It allows the dialog to display the hotkeys that have already been
8+
/// registered, and optionally provide a way to unregister them.
9+
/// </summary>
10+
public interface IHotkeySettings
11+
{
12+
/// <summary>
13+
/// A list of hotkeys that have already been registered. The dialog will display these hotkeys and provide a way to
14+
/// unregister them.
15+
/// </summary>
16+
public List<RegisteredHotkeyData> RegisteredHotkeys { get; }
17+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System;
2+
3+
namespace Flow.Launcher.Infrastructure.Hotkey;
4+
5+
#nullable enable
6+
7+
/// <summary>
8+
/// Represents a hotkey that has been registered. Used in <c>Flow.Launcher.HotkeyControlDialog</c> via
9+
/// <see cref="UserSettings"/> and <see cref="IHotkeySettings"/> to display errors if user tries to register a hotkey
10+
/// that has already been registered, and optionally provides a way to unregister the hotkey.
11+
/// </summary>
12+
public record RegisteredHotkeyData
13+
{
14+
/// <summary>
15+
/// <see cref="HotkeyModel"/> representation of this hotkey.
16+
/// </summary>
17+
public HotkeyModel Hotkey { get; }
18+
19+
/// <summary>
20+
/// String key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
21+
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
22+
/// </summary>
23+
public string DescriptionResourceKey { get; }
24+
25+
/// <summary>
26+
/// Array of values that will replace <c>{0}</c>, <c>{1}</c>, <c>{2}</c>, etc. in the localized string found via
27+
/// <see cref="DescriptionResourceKey"/>.
28+
/// </summary>
29+
public object?[] DescriptionFormatVariables { get; } = Array.Empty<object?>();
30+
31+
/// <summary>
32+
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that
33+
/// this hotkey can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
34+
/// </summary>
35+
public Action? RemoveHotkey { get; }
36+
37+
/// <summary>
38+
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
39+
/// <c>descriptionResourceKey</c> doesn't need any arguments for <c>string.Format</c>. If it does,
40+
/// use one of the other constructors.
41+
/// </summary>
42+
/// <param name="hotkey">
43+
/// The hotkey this class will represent.
44+
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
45+
/// </param>
46+
/// <param name="descriptionResourceKey">
47+
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
48+
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
49+
/// </param>
50+
/// <param name="removeHotkey">
51+
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
52+
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
53+
/// </param>
54+
public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action? removeHotkey = null)
55+
{
56+
Hotkey = new HotkeyModel(hotkey);
57+
DescriptionResourceKey = descriptionResourceKey;
58+
RemoveHotkey = removeHotkey;
59+
}
60+
61+
/// <summary>
62+
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
63+
/// <c>descriptionResourceKey</c> needs exactly one argument for <c>string.Format</c>.
64+
/// </summary>
65+
/// <param name="hotkey">
66+
/// The hotkey this class will represent.
67+
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
68+
/// </param>
69+
/// <param name="descriptionResourceKey">
70+
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
71+
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
72+
/// </param>
73+
/// <param name="descriptionFormatVariable">
74+
/// The value that will replace <c>{0}</c> in the localized string found via <c>description</c>.
75+
/// </param>
76+
/// <param name="removeHotkey">
77+
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
78+
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
79+
/// </param>
80+
public RegisteredHotkeyData(
81+
string hotkey, string descriptionResourceKey, object? descriptionFormatVariable, Action? removeHotkey = null
82+
)
83+
{
84+
Hotkey = new HotkeyModel(hotkey);
85+
DescriptionResourceKey = descriptionResourceKey;
86+
DescriptionFormatVariables = new[] { descriptionFormatVariable };
87+
RemoveHotkey = removeHotkey;
88+
}
89+
90+
/// <summary>
91+
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
92+
/// <paramref name="descriptionResourceKey"/> needs multiple arguments for <c>string.Format</c>.
93+
/// </summary>
94+
/// <param name="hotkey">
95+
/// The hotkey this class will represent.
96+
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
97+
/// </param>
98+
/// <param name="descriptionResourceKey">
99+
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
100+
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
101+
/// </param>
102+
/// <param name="descriptionFormatVariables">
103+
/// Array of values that will replace <c>{0}</c>, <c>{1}</c>, <c>{2}</c>, etc.
104+
/// in the localized string found via <c>description</c>.
105+
/// </param>
106+
/// <param name="removeHotkey">
107+
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
108+
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
109+
/// </param>
110+
public RegisteredHotkeyData(
111+
string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables, Action? removeHotkey = null
112+
)
113+
{
114+
Hotkey = new HotkeyModel(hotkey);
115+
DescriptionResourceKey = descriptionResourceKey;
116+
DescriptionFormatVariables = descriptionFormatVariables;
117+
RemoveHotkey = removeHotkey;
118+
}
119+
}

Flow.Launcher.Infrastructure/UserSettings/Settings.cs

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
using System.Drawing;
55
using System.Text.Json.Serialization;
66
using System.Windows;
7+
using Flow.Launcher.Infrastructure.Hotkey;
78
using Flow.Launcher.Plugin;
89
using Flow.Launcher.Plugin.SharedModels;
910
using Flow.Launcher.ViewModel;
1011

1112
namespace Flow.Launcher.Infrastructure.UserSettings
1213
{
13-
public class Settings : BaseModel
14+
public class Settings : BaseModel, IHotkeySettings
1415
{
1516
private string language = "en";
1617
private string _theme = Constant.DefaultTheme;
@@ -207,17 +208,17 @@ public string QuerySearchPrecisionString
207208

208209
public double WindowLeft { get; set; }
209210
public double WindowTop { get; set; }
210-
211+
211212
/// <summary>
212213
/// Custom left position on selected monitor
213214
/// </summary>
214215
public double CustomWindowLeft { get; set; } = 0;
215-
216+
216217
/// <summary>
217218
/// Custom top position on selected monitor
218219
/// </summary>
219220
public double CustomWindowTop { get; set; } = 0;
220-
221+
221222
public int MaxResultsToShow { get; set; } = 5;
222223
public int ActivateTimes { get; set; }
223224

@@ -229,7 +230,7 @@ public string QuerySearchPrecisionString
229230
[JsonIgnore]
230231
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
231232
{
232-
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
233+
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
233234
new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
234235
};
235236

@@ -253,7 +254,7 @@ public bool HideNotifyIcon
253254

254255
[JsonConverter(typeof(JsonStringEnumConverter))]
255256
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
256-
257+
257258
[JsonConverter(typeof(JsonStringEnumConverter))]
258259
public SearchWindowAligns SearchWindowAlign { get; set; } = SearchWindowAligns.Center;
259260

@@ -273,6 +274,82 @@ public bool HideNotifyIcon
273274

274275
// This needs to be loaded last by staying at the bottom
275276
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
277+
278+
[JsonIgnore]
279+
public List<RegisteredHotkeyData> RegisteredHotkeys
280+
{
281+
get
282+
{
283+
var list = new List<RegisteredHotkeyData>
284+
{
285+
new("Up", "HotkeyLeftRightDesc"),
286+
new("Down", "HotkeyLeftRightDesc"),
287+
new("Left", "HotkeyUpDownDesc"),
288+
new("Right", "HotkeyUpDownDesc"),
289+
new("Escape", "HotkeyESCDesc"),
290+
new("F5", "ReloadPluginHotkey"),
291+
new("Alt+Home", "HotkeySelectFirstResult"),
292+
new("Alt+End", "HotkeySelectLastResult"),
293+
new("Ctrl+R", "HotkeyRequery"),
294+
new("Ctrl+H", "ToggleHistoryHotkey"),
295+
new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"),
296+
new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"),
297+
new("Ctrl+OemPlus", "QuickHeightHotkey"),
298+
new("Ctrl+OemMinus", "QuickHeightHotkey"),
299+
new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"),
300+
new("Shift+Enter", "OpenContextMenuHotkey"),
301+
new("Enter", "HotkeyRunDesc"),
302+
new("Ctrl+Enter", "OpenContainFolderHotkey"),
303+
new("Alt+Enter", "HotkeyOpenResult"),
304+
new("Ctrl+F12", "ToggleGameModeHotkey"),
305+
new("Ctrl+Shift+C", "CopyFilePathHotkey"),
306+
307+
new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1),
308+
new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2),
309+
new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3),
310+
new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4),
311+
new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5),
312+
new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6),
313+
new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7),
314+
new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8),
315+
new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9),
316+
new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10)
317+
};
318+
319+
if(!string.IsNullOrEmpty(Hotkey))
320+
list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = ""));
321+
if(!string.IsNullOrEmpty(PreviewHotkey))
322+
list.Add(new(PreviewHotkey, "previewHotkey", () => PreviewHotkey = ""));
323+
if(!string.IsNullOrEmpty(AutoCompleteHotkey))
324+
list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = ""));
325+
if(!string.IsNullOrEmpty(AutoCompleteHotkey2))
326+
list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = ""));
327+
if(!string.IsNullOrEmpty(SelectNextItemHotkey))
328+
list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = ""));
329+
if(!string.IsNullOrEmpty(SelectNextItemHotkey2))
330+
list.Add(new(SelectNextItemHotkey2, "SelectNextItemHotkey", () => SelectNextItemHotkey2 = ""));
331+
if(!string.IsNullOrEmpty(SelectPrevItemHotkey))
332+
list.Add(new(SelectPrevItemHotkey, "SelectPrevItemHotkey", () => SelectPrevItemHotkey = ""));
333+
if(!string.IsNullOrEmpty(SelectPrevItemHotkey2))
334+
list.Add(new(SelectPrevItemHotkey2, "SelectPrevItemHotkey", () => SelectPrevItemHotkey2 = ""));
335+
if(!string.IsNullOrEmpty(SettingWindowHotkey))
336+
list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = ""));
337+
if(!string.IsNullOrEmpty(OpenContextMenuHotkey))
338+
list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = ""));
339+
if(!string.IsNullOrEmpty(SelectNextPageHotkey))
340+
list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = ""));
341+
if(!string.IsNullOrEmpty(SelectPrevPageHotkey))
342+
list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = ""));
343+
344+
foreach (var customPluginHotkey in CustomPluginHotkeys)
345+
{
346+
if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey))
347+
list.Add(new(customPluginHotkey.Hotkey, "customQueryHotkey", () => customPluginHotkey.Hotkey = ""));
348+
}
349+
350+
return list;
351+
}
352+
}
276353
}
277354

278355
public enum LastQueryMode
@@ -288,7 +365,7 @@ public enum ColorSchemes
288365
Light,
289366
Dark
290367
}
291-
368+
292369
public enum SearchWindowScreens
293370
{
294371
RememberLastLaunchLocation,
@@ -297,7 +374,7 @@ public enum SearchWindowScreens
297374
Primary,
298375
Custom
299376
}
300-
377+
301378
public enum SearchWindowAligns
302379
{
303380
Center,

Flow.Launcher/CustomQueryHotkeySetting.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
HorizontalAlignment="Left"
107107
VerticalAlignment="Center"
108108
HorizontalContentAlignment="Left"
109+
HotkeySettings="{Binding Settings}"
109110
DefaultHotkey="" />
110111
<TextBlock
111112
Grid.Row="1"

Flow.Launcher/CustomQueryHotkeySetting.xaml.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ public partial class CustomQueryHotkeySetting : Window
1515
private SettingWindow _settingWidow;
1616
private bool update;
1717
private CustomPluginHotkey updateCustomHotkey;
18-
private Settings _settings;
18+
public Settings Settings { get; }
1919

2020
public CustomQueryHotkeySetting(SettingWindow settingWidow, Settings settings)
2121
{
2222
_settingWidow = settingWidow;
23+
Settings = settings;
2324
InitializeComponent();
24-
_settings = settings;
2525
}
2626

2727
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
@@ -33,13 +33,13 @@ private void btnAdd_OnClick(object sender, RoutedEventArgs e)
3333
{
3434
if (!update)
3535
{
36-
_settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
36+
Settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
3737

3838
var pluginHotkey = new CustomPluginHotkey
3939
{
4040
Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text
4141
};
42-
_settings.CustomPluginHotkeys.Add(pluginHotkey);
42+
Settings.CustomPluginHotkeys.Add(pluginHotkey);
4343

4444
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
4545
}
@@ -59,7 +59,7 @@ private void btnAdd_OnClick(object sender, RoutedEventArgs e)
5959

6060
public void UpdateItem(CustomPluginHotkey item)
6161
{
62-
updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o =>
62+
updateCustomHotkey = Settings.CustomPluginHotkeys.FirstOrDefault(o =>
6363
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
6464
if (updateCustomHotkey == null)
6565
{

Flow.Launcher/HotkeyControl.xaml.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ namespace Flow.Launcher
1212
{
1313
public partial class HotkeyControl
1414
{
15+
public IHotkeySettings HotkeySettings {
16+
get { return (IHotkeySettings)GetValue(HotkeySettingsProperty); }
17+
set { SetValue(HotkeySettingsProperty, value); }
18+
}
19+
20+
public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register(
21+
nameof(HotkeySettings),
22+
typeof(IHotkeySettings),
23+
typeof(HotkeyControl),
24+
new PropertyMetadata()
25+
);
1526
public string WindowTitle {
1627
get { return (string)GetValue(WindowTitleProperty); }
1728
set { SetValue(WindowTitleProperty, value); }
@@ -122,7 +133,7 @@ private async Task OpenHotkeyDialog()
122133
HotKeyMapper.RemoveHotkey(Hotkey);
123134
}
124135

125-
var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle);
136+
var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, HotkeySettings, WindowTitle);
126137
await dialog.ShowAsync();
127138
switch (dialog.ResultType)
128139
{

0 commit comments

Comments
 (0)