This commit is contained in:
jamil 2026-03-09 20:53:41 -07:00 committed by GitHub
commit ad8de0c092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 2764 additions and 567 deletions

View file

@ -11,6 +11,7 @@ using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -32,14 +33,22 @@ namespace Flow.Launcher.Core.Plugin
private static readonly ConcurrentDictionary<string, PluginPair> _globalPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _nonGlobalPlugins = [];
public static event Action<PluginHotkeyChangedEvent> PluginHotkeyChanged;
public static event Action<PluginPair> PluginHotkeyInitialized;
private static PluginsSettings Settings;
private static readonly ConcurrentBag<string> ModifiedPlugins = [];
private static readonly ConcurrentBag<PluginPair> _contextMenuPlugins = [];
private static readonly ConcurrentBag<PluginPair> _homePlugins = [];
private static readonly ConcurrentBag<PluginPair> _translationPlugins = [];
private static readonly ConcurrentBag<PluginPair> _hotkeyPlugins = [];
private static readonly ConcurrentBag<PluginPair> _externalPreviewPlugins = [];
private static readonly Lock _pluginHotkeyInfoUpdateLock = new();
private static readonly ConcurrentDictionary<PluginPair, List<BasePluginHotkey>> _pluginHotkeyInfo = [];
private static readonly ConcurrentDictionary<HotkeyModel, ConcurrentBag<(PluginMetadata, SearchWindowPluginHotkey)>> _windowPluginHotkeys = [];
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
@ -303,6 +312,9 @@ namespace Flow.Launcher.Core.Plugin
// Add plugin to Dialog Jump plugin list after the plugin is initialized
DialogJump.InitializeDialogJumpPlugin(pair);
// Check and initialize plugin hotkeys after the plugin is initialized
CheckPluginHotkeys(pair);
// Add plugin to lists after the plugin is initialized
AddPluginToLists(pair);
}));
@ -360,6 +372,22 @@ namespace Flow.Launcher.Core.Plugin
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
}
private static void CheckPluginHotkeys(PluginPair pair)
{
if (pair.Plugin is IPluginHotkey)
{
InitializePluginHotkeyInfo(pair);
// Since settings cannot be changed concurrently, we must use a lock here
lock (_pluginHotkeyInfoUpdateLock)
{
Settings.UpdatePluginHotkeyInfo(GetPluginHotkeyInfo(pair.Metadata.ID));
}
InitializeWindowPluginHotkey(pair);
_hotkeyPlugins.Add(pair);
PluginHotkeyInitialized?.Invoke(pair);
}
}
#endregion
#region Validate & Query Plugins
@ -587,6 +615,11 @@ namespace Flow.Launcher.Core.Plugin
return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
public static List<PluginPair> GetHotkeyPlugins()
{
return [.. _hotkeyPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
#endregion
#region Update Metadata & Get Plugin
@ -792,6 +825,175 @@ namespace Flow.Launcher.Core.Plugin
#endregion
#region Plugin Hotkey
private static void InitializePluginHotkeyInfo(PluginPair pair)
{
var plugin = (IPluginHotkey)pair.Plugin;
var hotkeys = plugin.GetPluginHotkeys();
_pluginHotkeyInfo.TryAdd(pair, hotkeys);
}
private static void InitializeWindowPluginHotkey(PluginPair pair)
{
foreach (var info in GetPluginHotkeyInfo(pair.Metadata.ID))
{
var pluginPair = info.Key;
var hotkeyInfo = info.Value;
var metadata = pluginPair.Metadata;
foreach (var hotkey in hotkeyInfo)
{
if (hotkey.HotkeyType == HotkeyType.SearchWindow && hotkey is SearchWindowPluginHotkey searchWindowHotkey)
{
var hotkeySetting = metadata.PluginHotkeys.Find(h => h.Id == hotkey.Id)?.Hotkey ?? hotkey.DefaultHotkey;
var hotkeyModel = new HotkeyModel(hotkeySetting);
if (!_windowPluginHotkeys.TryGetValue(hotkeyModel, out var list))
{
list = [];
_windowPluginHotkeys[hotkeyModel] = list;
}
list.Add((pluginPair.Metadata, searchWindowHotkey));
}
}
}
}
public static Dictionary<PluginPair, List<BasePluginHotkey>> GetPluginHotkeyInfo(string id = null)
{
if (id == null)
{
// Return all plugin hotkey info except those from modified plugins
return _pluginHotkeyInfo.Where(p => !PluginModified(p.Key.Metadata.ID))
.ToDictionary(p => p.Key, p => p.Value);
}
else
{
// Return plugin hotkey info for specified plugin id
return _pluginHotkeyInfo.Where(p => p.Key.Metadata.ID == id)
.ToDictionary(p => p.Key, p => p.Value);
}
}
public static Dictionary<HotkeyModel, ConcurrentBag<(PluginMetadata Metadata, SearchWindowPluginHotkey SearchWindowPluginHotkey)>> GetWindowPluginHotkeys(string id = null)
{
// Here we do not need to check PluginModified since we will check it in hotkey events
if (id == null)
{
// Return all window plugin hotkeys
return _windowPluginHotkeys.ToDictionary(p => p.Key, p => p.Value);
}
else
{
// Return window plugin hotkeys for specified plugin id
// If one HotkeyModel is already included by other plugins, it will be removed so that HotkeyMapper will not register this Window hotkey duplicately
var windowPluginHotkeys = new Dictionary<HotkeyModel, ConcurrentBag<(PluginMetadata Metadata, SearchWindowPluginHotkey SearchWindowPluginHotkey)>>();
foreach (var key in _windowPluginHotkeys)
{
// Check if all items in the list are from the specified plugin
if (key.Value.All(x => x.Item1.ID == id))
{
// We must use the reference of this ConcurrentBag so that it can be updated in the next
windowPluginHotkeys[key.Key] = key.Value;
}
}
return windowPluginHotkeys;
}
}
public static void UpdatePluginHotkeyInfoTranslations(PluginPair pair)
{
var newHotkeys = ((IPluginHotkey)pair.Plugin).GetPluginHotkeys();
if (_pluginHotkeyInfo.TryGetValue(pair, out var oldHotkeys))
{
foreach (var newHotkey in newHotkeys)
{
if (oldHotkeys.FirstOrDefault(h => h.Id == newHotkey.Id) is BasePluginHotkey pluginHotkey)
{
pluginHotkey.Name = newHotkey.Name;
pluginHotkey.Description = newHotkey.Description;
}
else
{
oldHotkeys.Add(newHotkey);
}
}
}
else
{
_pluginHotkeyInfo.TryAdd(pair, newHotkeys);
}
}
public static void ChangePluginHotkey(PluginMetadata plugin, GlobalPluginHotkey pluginHotkey, HotkeyModel newHotkey)
{
var oldHotkeyItem = plugin.PluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var settingHotkeyItem = Settings.GetPluginSettings(plugin.ID).pluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var oldHotkeyStr = settingHotkeyItem.Hotkey;
var oldHotkey = new HotkeyModel(oldHotkeyStr);
var newHotkeyStr = newHotkey.ToString();
// Update hotkey in plugin metadata & setting
oldHotkeyItem.Hotkey = newHotkeyStr;
settingHotkeyItem.Hotkey = newHotkeyStr;
PluginHotkeyChanged?.Invoke(new PluginHotkeyChangedEvent(oldHotkey, newHotkey, plugin, pluginHotkey));
}
public static void ChangePluginHotkey(PluginMetadata plugin, SearchWindowPluginHotkey pluginHotkey, HotkeyModel newHotkey)
{
var oldHotkeyItem = plugin.PluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var settingHotkeyItem = Settings.GetPluginSettings(plugin.ID).pluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var oldHotkeyStr = settingHotkeyItem.Hotkey;
var oldHotkey = new HotkeyModel(oldHotkeyStr);
var newHotkeyStr = newHotkey.ToString();
// Update hotkey in plugin metadata & setting
oldHotkeyItem.Hotkey = newHotkeyStr;
settingHotkeyItem.Hotkey = newHotkeyStr;
// Update window plugin hotkey dictionary
var oldHotkeyModels = _windowPluginHotkeys[oldHotkey];
_windowPluginHotkeys[oldHotkey] = [.. oldHotkeyModels.Where(x => x.Item1.ID != plugin.ID || x.Item2.Id != pluginHotkey.Id)];
if (_windowPluginHotkeys[oldHotkey].IsEmpty)
{
_windowPluginHotkeys.TryRemove(oldHotkey, out var _);
}
if (_windowPluginHotkeys.TryGetValue(newHotkey, out var newHotkeyModels))
{
var newList = newHotkeyModels.ToList();
newList.Add((plugin, pluginHotkey));
_windowPluginHotkeys[newHotkey] = [.. newList];
}
else
{
_windowPluginHotkeys[newHotkey] =
[
(plugin, pluginHotkey)
];
}
PluginHotkeyChanged?.Invoke(new PluginHotkeyChangedEvent(oldHotkey, newHotkey, plugin, pluginHotkey));
}
#region Class
public class PluginHotkeyChangedEvent(HotkeyModel oldHotkey, HotkeyModel newHotkey,
PluginMetadata metadata, BasePluginHotkey pluginHotkey)
{
public HotkeyModel NewHotkey { get; } = newHotkey;
public HotkeyModel OldHotkey { get; } = oldHotkey;
public PluginMetadata Metadata { get; } = metadata;
public BasePluginHotkey PluginHotkey { get; } = pluginHotkey;
}
#endregion
#endregion
#region Plugin Install & Uninstall & Update
#region Private Functions

View file

@ -368,6 +368,11 @@ namespace Flow.Launcher.Core.Resource
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
if (p.Plugin is IPluginHotkey)
{
// Update plugin hotkey name & description
PluginManager.UpdatePluginHotkeyInfoTranslations(p);
}
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)
@ -385,6 +390,11 @@ namespace Flow.Launcher.Core.Resource
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
if (p.Plugin is IPluginHotkey)
{
// Update plugin hotkey name & description
PluginManager.UpdatePluginHotkeyInfoTranslations(p);
}
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)

View file

@ -1,19 +1,19 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.DialogJump.Models;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using NHotkey;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Accessibility;
using System.Collections.Concurrent;
namespace Flow.Launcher.Infrastructure.DialogJump
{
@ -464,10 +464,15 @@ namespace Flow.Launcher.Infrastructure.DialogJump
#endregion
#region Hotkey
#region Hotkey Command
public static void OnToggleHotkey(object sender, HotkeyEventArgs args)
private static RelayCommand _dialogJumpCommand;
public static IRelayCommand DialogJumpCommand => _dialogJumpCommand ??= new RelayCommand(OnToggleHotkey);
private static void OnToggleHotkey()
{
if (!_settings.EnableDialogJump) return;
_ = Task.Run(async () =>
{
try

View file

@ -49,6 +49,10 @@ namespace Flow.Launcher.Infrastructure.Hotkey
}
}
public readonly bool IsEmpty => CharKey == Key.None && !Alt && !Shift && !Win && !Ctrl;
public static HotkeyModel Empty => new();
public HotkeyModel(string hotkeyString)
{
Parse(hotkeyString);
@ -117,12 +121,22 @@ namespace Flow.Launcher.Infrastructure.Hotkey
}
}
public override string ToString()
public bool Equals(HotkeyModel other)
{
return CharKey == other.CharKey && ModifierKeys == other.ModifierKeys;
}
public KeyGesture ToKeyGesture()
{
return new KeyGesture(CharKey, ModifierKeys);
}
public override readonly string ToString()
{
return string.Join(" + ", EnumerateDisplayKeys());
}
public IEnumerable<string> EnumerateDisplayKeys()
public readonly IEnumerable<string> EnumerateDisplayKeys()
{
if (Ctrl && CharKey is not (Key.LeftCtrl or Key.RightCtrl))
{
@ -155,9 +169,9 @@ namespace Flow.Launcher.Infrastructure.Hotkey
/// <summary>
/// Validate hotkey
/// </summary>
/// <param name="validateKeyGestrue">Try to validate hotkey as a KeyGesture.</param>
/// <param name="validateKeyGesture">Try to validate hotkey as a KeyGesture.</param>
/// <returns></returns>
public bool Validate(bool validateKeyGestrue = false)
public bool Validate(bool validateKeyGesture = false)
{
switch (CharKey)
{
@ -172,7 +186,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey
case Key.None:
return false;
default:
if (validateKeyGestrue)
if (validateKeyGesture)
{
try
{

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Flow.Launcher.Infrastructure.Hotkey;
@ -13,5 +13,5 @@ public interface IHotkeySettings
/// A list of hotkeys that have already been registered. The dialog will display these hotkeys and provide a way to
/// unregister them.
/// </summary>
public List<RegisteredHotkeyData> RegisteredHotkeys { get; }
public ObservableCollection<RegisteredHotkeyData> RegisteredHotkeys { get; }
}

View file

@ -1,4 +1,6 @@
using System;
using System.Windows.Input;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.Hotkey;
@ -11,10 +13,20 @@ namespace Flow.Launcher.Infrastructure.Hotkey;
/// </summary>
public record RegisteredHotkeyData
{
/// <summary>
/// Type of this hotkey in the context of the application.
/// </summary>
public RegisteredHotkeyType RegisteredType { get; }
/// <summary>
/// Type of this hotkey.
/// </summary>
public HotkeyType Type { get; }
/// <summary>
/// <see cref="HotkeyModel"/> representation of this hotkey.
/// </summary>
public HotkeyModel Hotkey { get; }
public HotkeyModel Hotkey { get; private set; }
/// <summary>
/// String key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
@ -28,6 +40,16 @@ public record RegisteredHotkeyData
/// </summary>
public object?[] DescriptionFormatVariables { get; } = Array.Empty<object?>();
/// <summary>
/// Command of this hotkey. If it's <c>null</c>, the hotkey is assumed to be registered by system.
/// </summary>
public ICommand? Command { get; }
/// <summary>
/// Command parameter of this hotkey.
/// </summary>
public object? CommandParameter { get; }
/// <summary>
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that
/// this hotkey can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
@ -39,6 +61,12 @@ public record RegisteredHotkeyData
/// <c>descriptionResourceKey</c> doesn't need any arguments for <c>string.Format</c>. If it does,
/// use one of the other constructors.
/// </summary>
/// <param name="registeredType">
/// The type of this hotkey in the context of the application.
/// </param>
/// <param name="type">
/// Whether this hotkey is global or search window specific.
/// </param>
/// <param name="hotkey">
/// The hotkey this class will represent.
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
@ -47,14 +75,68 @@ public record RegisteredHotkeyData
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
/// </param>
/// <param name="command">
/// The command that will be executed when this hotkey is triggered. If it's <c>null</c>, the hotkey is assumed to be registered by system.
/// </param>
/// <param name="parameter">
/// The command parameter that will be passed to the command when this hotkey is triggered. If it's <c>null</c>, no parameter will be passed.
/// </param>
/// <param name="removeHotkey">
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
/// </param>
public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action? removeHotkey = null)
public RegisteredHotkeyData(
RegisteredHotkeyType registeredType, HotkeyType type, string hotkey, string descriptionResourceKey,
ICommand? command, object? parameter = null, Action? removeHotkey = null)
{
RegisteredType = registeredType;
Type = type;
Hotkey = new HotkeyModel(hotkey);
DescriptionResourceKey = descriptionResourceKey;
Command = command;
CommandParameter = parameter;
RemoveHotkey = removeHotkey;
}
/// <summary>
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
/// <c>descriptionResourceKey</c> doesn't need any arguments for <c>string.Format</c>. If it does,
/// use one of the other constructors.
/// </summary>
/// <param name="registeredType">
/// The type of this hotkey in the context of the application.
/// </param>
/// <param name="type">
/// Whether this hotkey is global or search window specific.
/// </param>
/// <param name="hotkey">
/// The hotkey this class will represent.
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
/// </param>
/// <param name="descriptionResourceKey">
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
/// </param>
/// <param name="command">
/// The command that will be executed when this hotkey is triggered. If it's <c>null</c>, the hotkey is assumed to be registered by system.
/// </param>
/// <param name="parameter">
/// The command parameter that will be passed to the command when this hotkey is triggered. If it's <c>null</c>, no parameter will be passed.
/// </param>
/// <param name="removeHotkey">
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
/// </param>
public RegisteredHotkeyData(
RegisteredHotkeyType registeredType, HotkeyType type, HotkeyModel hotkey, string descriptionResourceKey,
ICommand? command, object? parameter = null, Action? removeHotkey = null)
{
RegisteredType = registeredType;
Type = type;
Hotkey = hotkey;
DescriptionResourceKey = descriptionResourceKey;
Command = command;
CommandParameter = parameter;
RemoveHotkey = removeHotkey;
}
@ -62,6 +144,12 @@ public record RegisteredHotkeyData
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
/// <c>descriptionResourceKey</c> needs exactly one argument for <c>string.Format</c>.
/// </summary>
/// <param name="registeredType">
/// The type of this hotkey in the context of the application.
/// </param>
/// <param name="type">
/// Whether this hotkey is global or search window specific.
/// </param>
/// <param name="hotkey">
/// The hotkey this class will represent.
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
@ -73,17 +161,28 @@ public record RegisteredHotkeyData
/// <param name="descriptionFormatVariable">
/// The value that will replace <c>{0}</c> in the localized string found via <c>description</c>.
/// </param>
/// <param name="command">
/// The command that will be executed when this hotkey is triggered. If it's <c>null</c>, the hotkey is assumed to be registered by system.
/// </param>
/// <param name="parameter">
/// The command parameter that will be passed to the command when this hotkey is triggered. If it's <c>null</c>, no parameter will be passed.
/// </param>
/// <param name="removeHotkey">
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
/// </param>
public RegisteredHotkeyData(
string hotkey, string descriptionResourceKey, object? descriptionFormatVariable, Action? removeHotkey = null
RegisteredHotkeyType registeredType, HotkeyType type, string hotkey, string descriptionResourceKey, object? descriptionFormatVariable,
ICommand? command, object? parameter = null, Action? removeHotkey = null
)
{
RegisteredType = registeredType;
Type = type;
Hotkey = new HotkeyModel(hotkey);
DescriptionResourceKey = descriptionResourceKey;
DescriptionFormatVariables = new[] { descriptionFormatVariable };
Command = command;
CommandParameter = parameter;
RemoveHotkey = removeHotkey;
}
@ -91,6 +190,12 @@ public record RegisteredHotkeyData
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
/// <paramref name="descriptionResourceKey"/> needs multiple arguments for <c>string.Format</c>.
/// </summary>
/// <param name="registeredType">
/// The type of this hotkey in the context of the application.
/// </param>
/// <param name="type">
/// Whether this hotkey is global or search window specific.
/// </param>
/// <param name="hotkey">
/// The hotkey this class will represent.
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
@ -103,17 +208,103 @@ public record RegisteredHotkeyData
/// Array of values that will replace <c>{0}</c>, <c>{1}</c>, <c>{2}</c>, etc.
/// in the localized string found via <c>description</c>.
/// </param>
/// <param name="command">
/// The command that will be executed when this hotkey is triggered. If it's <c>null</c>, the hotkey is assumed to be registered by system.
/// </param>
/// <param name="parameter">
/// The command parameter that will be passed to the command when this hotkey is triggered. If it's <c>null</c>, no parameter will be passed.
/// </param>
/// <param name="removeHotkey">
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
/// </param>
public RegisteredHotkeyData(
string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables, Action? removeHotkey = null
RegisteredHotkeyType registeredType, HotkeyType type, string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables,
ICommand? command, object? parameter = null, Action? removeHotkey = null
)
{
RegisteredType = registeredType;
Type = type;
Hotkey = new HotkeyModel(hotkey);
DescriptionResourceKey = descriptionResourceKey;
DescriptionFormatVariables = descriptionFormatVariables;
Command = command;
CommandParameter = parameter;
RemoveHotkey = removeHotkey;
}
/// <summary>
/// Sets the hotkey for this registered hotkey data.
/// </summary>
/// <param name="hotkey"></param>
public void SetHotkey(HotkeyModel hotkey)
{
Hotkey = hotkey;
}
/// <inheritdoc />
public override string ToString()
{
return Hotkey.IsEmpty ? $"{RegisteredType} - None" : $"{RegisteredType} - {Hotkey}";
}
}
public enum RegisteredHotkeyType
{
CtrlShiftEnter,
CtrlEnter,
AltEnter,
Up,
Down,
Left,
Right,
Esc,
Reload,
SelectFirstResult,
SelectLastResult,
ReQuery,
IncreaseWidth,
DecreaseWidth,
IncreaseMaxResult,
DecreaseMaxResult,
ShiftEnter,
Enter,
ToggleGameMode,
CopyFilePath,
OpenResultN1,
OpenResultN2,
OpenResultN3,
OpenResultN4,
OpenResultN5,
OpenResultN6,
OpenResultN7,
OpenResultN8,
OpenResultN9,
OpenResultN10,
Toggle,
DialogJump,
Preview,
AutoComplete,
AutoComplete2,
SelectNextItem,
SelectNextItem2,
SelectPrevItem,
SelectPrevItem2,
SettingWindow,
OpenHistory,
OpenContextMenu,
SelectNextPage,
SelectPrevPage,
CycleHistoryUp,
CycleHistoryDown,
CustomQuery,
PluginGlobalHotkey,
PluginWindowHotkey,
}

View file

@ -89,6 +89,116 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
}
/// <summary>
/// Update plugin hotkey information in metadata and plugin setting.
/// </summary>
/// <param name="hotkeyPluginInfo"></param>
public void UpdatePluginHotkeyInfo(IDictionary<PluginPair, List<BasePluginHotkey>> hotkeyPluginInfo)
{
foreach (var info in hotkeyPluginInfo)
{
var pluginPair = info.Key;
var hotkeyInfo = info.Value;
var metadata = pluginPair.Metadata;
metadata.PluginHotkeys ??= [];
metadata.PluginHotkeys.Clear();
if (Plugins.TryGetValue(pluginPair.Metadata.ID, out var plugin))
{
if (plugin.pluginHotkeys == null || plugin.pluginHotkeys.Count == 0)
{
// If plugin hotkeys does not exist, create a new one and initialize with default values
plugin.pluginHotkeys = [];
foreach (var hotkey in hotkeyInfo)
{
plugin.pluginHotkeys.Add(new PluginHotkey
{
Id = hotkey.Id,
DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values
Hotkey = hotkey.DefaultHotkey // use default value
});
metadata.PluginHotkeys.Add(new PluginHotkey
{
Id = hotkey.Id,
DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values
Hotkey = hotkey.DefaultHotkey // use default value
});
}
}
else
{
// If plugin hotkeys exist, update the existing hotkeys with the new values
foreach (var hotkey in hotkeyInfo)
{
var existingHotkey = plugin.pluginHotkeys.Find(h => h.Id == hotkey.Id);
if (existingHotkey != null)
{
// Update existing hotkey
existingHotkey.DefaultHotkey = hotkey.DefaultHotkey; // hotkey info provides default values
if (!hotkey.Editable) // If this hotkey is not editable anymore, we need to restore the hotkey
{
existingHotkey.Hotkey = hotkey.DefaultHotkey;
}
metadata.PluginHotkeys.Add(new PluginHotkey
{
Id = hotkey.Id,
DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values
Hotkey = existingHotkey.Hotkey // use settings value
});
}
else
{
// Add new hotkey if it does not exist
plugin.pluginHotkeys.Add(new PluginHotkey
{
Id = hotkey.Id,
DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values
Hotkey = hotkey.DefaultHotkey // use default value
});
metadata.PluginHotkeys.Add(new PluginHotkey
{
Id = hotkey.Id,
DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values
Hotkey = hotkey.DefaultHotkey // use default value
});
}
}
}
}
else
{
// If settings does not exist, create a new one
Plugins[metadata.ID] = new Plugin
{
ID = metadata.ID,
Name = metadata.Name,
Version = metadata.Version,
DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values
ActionKeywords = metadata.ActionKeywords, // use default value
Disabled = metadata.Disabled,
HomeDisabled = metadata.HomeDisabled,
Priority = metadata.Priority,
DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values
SearchDelayTime = metadata.SearchDelayTime, // use default value
};
foreach (var hotkey in hotkeyInfo)
{
Plugins[metadata.ID].pluginHotkeys.Add(new PluginHotkey
{
Id = hotkey.Id,
DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values
Hotkey = hotkey.DefaultHotkey // use default value
});
metadata.PluginHotkeys.Add(new PluginHotkey
{
Id = hotkey.Id,
DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values
Hotkey = hotkey.DefaultHotkey // use default value
});
}
}
}
}
public Plugin GetPluginSettings(string id)
{
if (Plugins.TryGetValue(id, out var plugin))
@ -126,6 +236,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public int? SearchDelayTime { get; set; }
public List<PluginHotkey> pluginHotkeys { get; set; } = new List<PluginHotkey>();
/// <summary>
/// Used only to save the state of the plugin in settings
/// </summary>

View file

@ -39,8 +39,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
_storage.Save();
}
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
private string _openResultModifiers = KeyConstant.Alt;
public string OpenResultModifiers
{
@ -72,21 +70,230 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
public double WindowSize { get; set; } = 580;
public string PreviewHotkey { get; set; } = $"F1";
public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab";
public string AutoCompleteHotkey2 { get; set; } = $"";
public string SelectNextItemHotkey { get; set; } = $"Tab";
public string SelectNextItemHotkey2 { get; set; } = $"";
public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab";
public string SelectPrevItemHotkey2 { get; set; } = $"";
public string SelectNextPageHotkey { get; set; } = $"PageUp";
public string SelectPrevPageHotkey { get; set; } = $"PageDown";
public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O";
public string SettingWindowHotkey { get; set; } = $"Ctrl+I";
public string OpenHistoryHotkey { get; set; } = $"Ctrl+H";
public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up";
public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down";
public string DialogJumpHotkey { get; set; } = $"{KeyConstant.Alt} + G";
private string _hotkey = $"{KeyConstant.Alt} + {KeyConstant.Space}";
public string Hotkey
{
get => _hotkey;
set
{
if (_hotkey != value)
{
_hotkey = value;
OnPropertyChanged();
}
}
}
private string _previewHotkey = "F1";
public string PreviewHotkey
{
get => _previewHotkey;
set
{
if (_previewHotkey != value)
{
_previewHotkey = value;
OnPropertyChanged();
}
}
}
private string _autoCompleteHotkey = $"{KeyConstant.Ctrl} + Tab";
public string AutoCompleteHotkey
{
get => _autoCompleteHotkey;
set
{
if (_autoCompleteHotkey != value)
{
_autoCompleteHotkey = value;
OnPropertyChanged();
}
}
}
private string _autoCompleteHotkey2 = "";
public string AutoCompleteHotkey2
{
get => _autoCompleteHotkey2;
set
{
if (_autoCompleteHotkey2 != value)
{
_autoCompleteHotkey2 = value;
OnPropertyChanged();
}
}
}
private string _selectNextItemHotkey = "Tab";
public string SelectNextItemHotkey
{
get => _selectNextItemHotkey;
set
{
if (_selectNextItemHotkey != value)
{
_selectNextItemHotkey = value;
OnPropertyChanged();
}
}
}
private string _selectNextItemHotkey2 = "";
public string SelectNextItemHotkey2
{
get => _selectNextItemHotkey2;
set
{
if (_selectNextItemHotkey2 != value)
{
_selectNextItemHotkey2 = value;
OnPropertyChanged();
}
}
}
private string _selectPrevItemHotkey = "Shift + Tab";
public string SelectPrevItemHotkey
{
get => _selectPrevItemHotkey;
set
{
if (_selectPrevItemHotkey != value)
{
_selectPrevItemHotkey = value;
OnPropertyChanged();
}
}
}
private string _selectPrevItemHotkey2 = "";
public string SelectPrevItemHotkey2
{
get => _selectPrevItemHotkey2;
set
{
if (_selectPrevItemHotkey2 != value)
{
_selectPrevItemHotkey2 = value;
OnPropertyChanged();
}
}
}
private string _selectNextPageHotkey = "PageUp";
public string SelectNextPageHotkey
{
get => _selectNextPageHotkey;
set
{
if (_selectNextPageHotkey != value)
{
_selectNextPageHotkey = value;
OnPropertyChanged();
}
}
}
private string _selectPrevPageHotkey = "PageDown";
public string SelectPrevPageHotkey
{
get => _selectPrevPageHotkey;
set
{
if (_selectPrevPageHotkey != value)
{
_selectPrevPageHotkey = value;
OnPropertyChanged();
}
}
}
private string _openContextMenuHotkey = "Ctrl+O";
public string OpenContextMenuHotkey
{
get => _openContextMenuHotkey;
set
{
if (_openContextMenuHotkey != value)
{
_openContextMenuHotkey = value;
OnPropertyChanged();
}
}
}
private string _settingWindowHotkey = "Ctrl+I";
public string SettingWindowHotkey
{
get => _settingWindowHotkey;
set
{
if (_settingWindowHotkey != value)
{
_settingWindowHotkey = value;
OnPropertyChanged();
}
}
}
private string _openHistoryHotkey = "Ctrl+H";
public string OpenHistoryHotkey
{
get => _openHistoryHotkey;
set
{
if (_openHistoryHotkey != value)
{
_openHistoryHotkey = value;
OnPropertyChanged();
}
}
}
private string _cycleHistoryUpHotkey = $"{KeyConstant.Alt} + Up";
public string CycleHistoryUpHotkey
{
get => _cycleHistoryUpHotkey;
set
{
if (_cycleHistoryUpHotkey != value)
{
_cycleHistoryUpHotkey = value;
OnPropertyChanged();
}
}
}
private string _cycleHistoryDownHotkey = $"{KeyConstant.Alt} + Down";
public string CycleHistoryDownHotkey
{
get => _cycleHistoryDownHotkey;
set
{
if (_cycleHistoryDownHotkey != value)
{
_cycleHistoryDownHotkey = value;
OnPropertyChanged();
}
}
}
private string _dialogJumpHotkey = $"{KeyConstant.Alt} + G";
public string DialogJumpHotkey
{
get => _dialogJumpHotkey;
set
{
if (_dialogJumpHotkey != value)
{
_dialogJumpHotkey = value;
OnPropertyChanged();
}
}
}
private string _language = Constant.SystemLanguageCode;
public string Language
@ -324,7 +531,19 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
};
public bool EnableDialogJump { get; set; } = true;
private bool _enableDialogJump = true;
public bool EnableDialogJump
{
get => _enableDialogJump;
set
{
if (_enableDialogJump != value)
{
_enableDialogJump = value;
OnPropertyChanged();
}
}
}
public bool AutoDialogJump { get; set; } = false;
@ -449,9 +668,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public int ActivateTimes { get; set; }
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new();
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>();
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new();
[JsonIgnore]
public ObservableCollection<BaseBuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
@ -538,97 +757,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool WMPInstalled { get; set; } = true;
// This needs to be loaded last by staying at the bottom
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
public PluginsSettings PluginSettings { get; set; } = new();
[JsonIgnore]
public List<RegisteredHotkeyData> RegisteredHotkeys
{
get
{
var list = FixedHotkeys();
// Customizable hotkeys
if (!string.IsNullOrEmpty(Hotkey))
list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = ""));
if (!string.IsNullOrEmpty(PreviewHotkey))
list.Add(new(PreviewHotkey, "previewHotkey", () => PreviewHotkey = ""));
if (!string.IsNullOrEmpty(AutoCompleteHotkey))
list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = ""));
if (!string.IsNullOrEmpty(AutoCompleteHotkey2))
list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = ""));
if (!string.IsNullOrEmpty(SelectNextItemHotkey))
list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = ""));
if (!string.IsNullOrEmpty(SelectNextItemHotkey2))
list.Add(new(SelectNextItemHotkey2, "SelectNextItemHotkey", () => SelectNextItemHotkey2 = ""));
if (!string.IsNullOrEmpty(SelectPrevItemHotkey))
list.Add(new(SelectPrevItemHotkey, "SelectPrevItemHotkey", () => SelectPrevItemHotkey = ""));
if (!string.IsNullOrEmpty(SelectPrevItemHotkey2))
list.Add(new(SelectPrevItemHotkey2, "SelectPrevItemHotkey", () => SelectPrevItemHotkey2 = ""));
if (!string.IsNullOrEmpty(SettingWindowHotkey))
list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = ""));
if (!string.IsNullOrEmpty(OpenHistoryHotkey))
list.Add(new(OpenHistoryHotkey, "OpenHistoryHotkey", () => OpenHistoryHotkey = ""));
if (!string.IsNullOrEmpty(OpenContextMenuHotkey))
list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = ""));
if (!string.IsNullOrEmpty(SelectNextPageHotkey))
list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = ""));
if (!string.IsNullOrEmpty(SelectPrevPageHotkey))
list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = ""));
if (!string.IsNullOrEmpty(CycleHistoryUpHotkey))
list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = ""));
if (!string.IsNullOrEmpty(CycleHistoryDownHotkey))
list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = ""));
if (!string.IsNullOrEmpty(DialogJumpHotkey))
list.Add(new(DialogJumpHotkey, "dialogJumpHotkey", () => DialogJumpHotkey = ""));
// Custom Query Hotkeys
foreach (var customPluginHotkey in CustomPluginHotkeys)
{
if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey))
list.Add(new(customPluginHotkey.Hotkey, "customQueryHotkey", () => customPluginHotkey.Hotkey = ""));
}
return list;
}
}
private List<RegisteredHotkeyData> FixedHotkeys()
{
return new List<RegisteredHotkeyData>
{
new("Up", "HotkeyLeftRightDesc"),
new("Down", "HotkeyLeftRightDesc"),
new("Left", "HotkeyUpDownDesc"),
new("Right", "HotkeyUpDownDesc"),
new("Escape", "HotkeyESCDesc"),
new("F5", "ReloadPluginHotkey"),
new("Alt+Home", "HotkeySelectFirstResult"),
new("Alt+End", "HotkeySelectLastResult"),
new("Ctrl+R", "HotkeyRequery"),
new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"),
new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"),
new("Ctrl+OemPlus", "QuickHeightHotkey"),
new("Ctrl+OemMinus", "QuickHeightHotkey"),
new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"),
new("Shift+Enter", "OpenContextMenuHotkey"),
new("Enter", "HotkeyRunDesc"),
new("Ctrl+Enter", "OpenContainFolderHotkey"),
new("Alt+Enter", "HotkeyOpenResult"),
new("Ctrl+F12", "ToggleGameModeHotkey"),
new("Ctrl+Shift+C", "CopyFilePathHotkey"),
new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1),
new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2),
new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3),
new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4),
new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5),
new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6),
new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7),
new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8),
new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9),
new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10)
};
}
public ObservableCollection<RegisteredHotkeyData> RegisteredHotkeys { get; } = new();
}
public enum LastQueryMode

View file

@ -1,4 +1,5 @@
using System.Windows.Input;
using System;
using System.Windows.Input;
namespace Flow.Launcher.Plugin
{
@ -6,6 +7,7 @@ namespace Flow.Launcher.Plugin
/// Context provided as a parameter when invoking a
/// <see cref="Result.Action"/> or <see cref="Result.AsyncAction"/>
/// </summary>
[Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")]
public class ActionContext
{
/// <summary>

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Represent plugins that support global hotkey or search window hotkey.
/// </summary>
public interface IPluginHotkey : IFeatures
{
/// <summary>
/// Get the list of plugin hotkeys which will be registered in the settings page.
/// </summary>
/// <returns></returns>
List<BasePluginHotkey> GetPluginHotkeys();
}
}

View file

@ -0,0 +1,134 @@
using System;
namespace Flow.Launcher.Plugin;
/// <summary>
/// Represents a base plugin hotkey model.
/// </summary>
/// <remarks>
/// Do not use this class directly. Use <see cref="GlobalPluginHotkey"/> or <see cref="SearchWindowPluginHotkey"/> instead.
/// </remarks>
public class BasePluginHotkey
{
/// <summary>
/// Initializes a new instance of the <see cref="BasePluginHotkey"/> class with the specified hotkey type.
/// </summary>
/// <param name="type">The type of hotkey (Global or SearchWindow).</param>
protected BasePluginHotkey(HotkeyType type)
{
HotkeyType = type;
}
/// <summary>
/// The unique identifier for the hotkey, which is used to identify and rank the hotkey in the settings page.
/// </summary>
public int Id { get; set; } = 0;
/// <summary>
/// The name of the hotkey, which will be displayed in the settings page.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The description of the hotkey, which will be displayed in the settings page.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// The glyph information for the hotkey, which will be displayed in the settings page.
/// </summary>
public GlyphInfo Glyph { get; set; }
/// <summary>
/// The default hotkey that will be used if the user does not set a custom hotkey.
/// </summary>
public string DefaultHotkey { get; set; } = string.Empty;
/// <summary>
/// The type of the hotkey, which can be either global or search window specific.
/// </summary>
public HotkeyType HotkeyType { get; } = HotkeyType.Global;
/// <summary>
/// Indicates whether the hotkey is editable by the user in the settings page.
/// </summary>
public bool Editable { get; set; } = false;
/// <summary>
/// Whether to show the hotkey in the settings page.
/// </summary>
public bool Visible { get; set; } = true;
}
/// <summary>
/// Represents a global plugin hotkey model.
/// </summary>
public class GlobalPluginHotkey : BasePluginHotkey
{
/// <summary>
/// Initializes a new instance of the <see cref="GlobalPluginHotkey"/> class.
/// </summary>
public GlobalPluginHotkey() : base(HotkeyType.Global)
{
}
/// <summary>
/// An action that will be executed when the hotkey is triggered.
/// </summary>
public Action Action { get; set; } = null;
}
/// <summary>
/// Represents a plugin hotkey that is specific to the search window.
/// </summary>
public class SearchWindowPluginHotkey : BasePluginHotkey
{
/// <summary>
/// Initializes a new instance of the <see cref="SearchWindowPluginHotkey"/> class.
/// </summary>
public SearchWindowPluginHotkey() : base(HotkeyType.SearchWindow)
{
}
/// <summary>
/// An action that will be executed when the hotkey is triggered and a result is selected.
/// </summary>
public Func<Result, bool> Action { get; set; } = null;
}
/// <summary>
/// Represents the type of hotkey for a plugin.
/// </summary>
public enum HotkeyType
{
/// <summary>
/// A hotkey that will be triggered globally, regardless of the active window.
/// </summary>
Global,
/// <summary>
/// A hotkey that will be triggered only when the search window is active.
/// </summary>
SearchWindow
}
/// <summary>
/// Represents a plugin hotkey model which is used to store the hotkey information for a plugin.
/// </summary>
public class PluginHotkey
{
/// <summary>
/// The unique identifier for the hotkey.
/// </summary>
public int Id { get; set; } = 0;
/// <summary>
/// The default hotkey that will be used if the user does not set a custom hotkey.
/// </summary>
public string DefaultHotkey { get; set; } = string.Empty;
/// <summary>
/// The current hotkey that the user has set for the plugin.
/// </summary>
public string Hotkey { get; set; } = string.Empty;
}

View file

@ -157,6 +157,11 @@ namespace Flow.Launcher.Plugin
/// </summary>
public string PluginCacheDirectoryPath { get; internal set; }
/// <summary>
/// List of registered plugin hotkeys.
/// </summary>
public List<PluginHotkey> PluginHotkeys { get; set; } = new List<PluginHotkey>();
/// <summary>
/// Convert <see cref="PluginMetadata"/> to string.
/// </summary>

View file

@ -317,6 +317,12 @@ namespace Flow.Launcher.Plugin
/// </remarks>
public string QuerySuggestionText { get; set; }
/// <summary>
/// List of hotkey IDs that are supported for this result.
/// Those hotkeys should be registered by IPluginHotkey interface.
/// </summary>
public IList<int> HotkeyIds { get; set; } = new List<int>();
/// <summary>
/// Run this result, asynchronously
/// </summary>
@ -368,7 +374,8 @@ namespace Flow.Launcher.Plugin
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey,
ShowBadge = ShowBadge,
QuerySuggestionText = QuerySuggestionText
QuerySuggestionText = QuerySuggestionText,
HotkeyIds = HotkeyIds,
};
}

View file

@ -487,5 +487,46 @@ namespace Flow.Launcher.Plugin.SharedCommands
}
}
}
/// <summary>
/// Returns true if the given name is a valid file name
/// </summary>
public static bool IsValidFileName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return false;
if (IsReservedName(name)) return false;
if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
{
return false;
}
return true;
}
/// <summary>
/// Returns true if the given name is a valid name for a directory, not a path
/// </summary>
public static bool IsValidDirectoryName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return false;
if (IsReservedName(name)) return false;
var invalidChars = Path.GetInvalidPathChars().Concat(new[] { '/', '\\' }).ToArray();
if (name.IndexOfAny(invalidChars) >= 0)
{
return false;
}
return true;
}
private static readonly string[] ReservedNames = new[] { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
private static bool IsReservedName(string name)
{
var nameWithoutExtension = Path.GetFileNameWithoutExtension(name).ToUpperInvariant();
if (ReservedNames.Contains(nameWithoutExtension))
{
return true;
}
return false;
}
}
}

View file

@ -1,15 +1,27 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using ChefKeys;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
using NHotkey;
using NHotkey.Wpf;
namespace Flow.Launcher.Helper;
/// <summary>
/// Set Flow Launcher global hotkeys & window hotkeys
/// </summary>
internal static class HotKeyMapper
{
private static readonly string ClassName = nameof(HotKeyMapper);
@ -17,48 +29,461 @@ internal static class HotKeyMapper
private static Settings _settings;
private static MainViewModel _mainViewModel;
#region Initialization
internal static void Initialize()
{
_mainViewModel = Ioc.Default.GetRequiredService<MainViewModel>();
_settings = Ioc.Default.GetService<Settings>();
SetHotkey(_settings.Hotkey, OnToggleHotkey);
if (_settings.EnableDialogJump)
InitializeActionContextHotkeys();
InitializeRegisteredHotkeys();
_settings.PropertyChanged += Settings_PropertyChanged;
_settings.CustomPluginHotkeys.CollectionChanged += CustomPluginHotkeys_CollectionChanged;
PluginManager.PluginHotkeyChanged += PluginManager_PluginHotkeyChanged;
PluginManager.PluginHotkeyInitialized += PluginManager_PluginHotkeyInitialized;
}
private static void InitializeRegisteredHotkeys()
{
// Fixed hotkeys & Editable hotkeys
var list = new List<RegisteredHotkeyData>
{
SetHotkey(_settings.DialogJumpHotkey, DialogJump.OnToggleHotkey);
// System default window hotkeys
// Here the description of Up/Down and Left/Right are swapped - it is intentional
new(RegisteredHotkeyType.Up, HotkeyType.SearchWindow, "Up", nameof(Localize.HotkeyLeftRightDesc), null),
new(RegisteredHotkeyType.Down, HotkeyType.SearchWindow, "Down", nameof(Localize.HotkeyLeftRightDesc), null),
new(RegisteredHotkeyType.Left, HotkeyType.SearchWindow, "Left", nameof(Localize.HotkeyUpDownDesc), null),
new(RegisteredHotkeyType.Right, HotkeyType.SearchWindow, "Right", nameof(Localize.HotkeyUpDownDesc), null),
// Flow Launcher window hotkeys
new(RegisteredHotkeyType.Esc, HotkeyType.SearchWindow, "Escape", nameof(Localize.HotkeyESCDesc), _mainViewModel.EscCommand),
new(RegisteredHotkeyType.Reload, HotkeyType.SearchWindow, "F5", nameof(Localize.ReloadPluginHotkey), _mainViewModel.ReloadPluginDataCommand),
new(RegisteredHotkeyType.SelectFirstResult, HotkeyType.SearchWindow, "Alt+Home", nameof(Localize.HotkeySelectFirstResult), _mainViewModel.SelectFirstResultCommand),
new(RegisteredHotkeyType.SelectLastResult, HotkeyType.SearchWindow, "Alt+End", nameof(Localize.HotkeySelectLastResult), _mainViewModel.SelectLastResultCommand),
new(RegisteredHotkeyType.ReQuery, HotkeyType.SearchWindow, "Ctrl+R", nameof(Localize.HotkeyRequery), _mainViewModel.ReQueryCommand),
new(RegisteredHotkeyType.IncreaseWidth, HotkeyType.SearchWindow, "Ctrl+OemCloseBrackets", nameof(Localize.QuickWidthHotkey), _mainViewModel.IncreaseWidthCommand),
new(RegisteredHotkeyType.DecreaseWidth, HotkeyType.SearchWindow, "Ctrl+OemOpenBrackets", nameof(Localize.QuickWidthHotkey), _mainViewModel.DecreaseWidthCommand),
new(RegisteredHotkeyType.IncreaseMaxResult, HotkeyType.SearchWindow, "Ctrl+OemPlus", nameof(Localize.QuickHeightHotkey), _mainViewModel.IncreaseMaxResultCommand),
new(RegisteredHotkeyType.DecreaseMaxResult, HotkeyType.SearchWindow, "Ctrl+OemMinus", nameof(Localize.QuickHeightHotkey), _mainViewModel.DecreaseMaxResultCommand),
new(RegisteredHotkeyType.ShiftEnter, HotkeyType.SearchWindow, "Shift+Enter", nameof(Localize.OpenContextMenuHotkey), _mainViewModel.LoadContextMenuCommand),
new(RegisteredHotkeyType.Enter, HotkeyType.SearchWindow, "Enter", nameof(Localize.HotkeyRunDesc), _mainViewModel.OpenResultCommand),
new(RegisteredHotkeyType.ToggleGameMode, HotkeyType.SearchWindow, "Ctrl+F12", nameof(Localize.ToggleGameModeHotkey), _mainViewModel.ToggleGameModeCommand),
new(RegisteredHotkeyType.CopyFilePath, HotkeyType.SearchWindow, "Ctrl+Shift+C", nameof(Localize.CopyFilePathHotkey), _mainViewModel.CopyAlternativeCommand),
// Result Modifier Hotkeys
new(RegisteredHotkeyType.OpenResultN1, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D1", nameof(Localize.HotkeyOpenResultN), 1, _mainViewModel.OpenResultCommand, "0"),
new(RegisteredHotkeyType.OpenResultN2, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D2", nameof(Localize.HotkeyOpenResultN), 2, _mainViewModel.OpenResultCommand, "1"),
new(RegisteredHotkeyType.OpenResultN3, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D3", nameof(Localize.HotkeyOpenResultN), 3, _mainViewModel.OpenResultCommand, "2"),
new(RegisteredHotkeyType.OpenResultN4, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D4", nameof(Localize.HotkeyOpenResultN), 4, _mainViewModel.OpenResultCommand, "3"),
new(RegisteredHotkeyType.OpenResultN5, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D5", nameof(Localize.HotkeyOpenResultN), 5, _mainViewModel.OpenResultCommand, "4"),
new(RegisteredHotkeyType.OpenResultN6, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D6", nameof(Localize.HotkeyOpenResultN), 6, _mainViewModel.OpenResultCommand, "5"),
new(RegisteredHotkeyType.OpenResultN7, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D7", nameof(Localize.HotkeyOpenResultN), 7, _mainViewModel.OpenResultCommand, "6"),
new(RegisteredHotkeyType.OpenResultN8, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D8", nameof(Localize.HotkeyOpenResultN), 8, _mainViewModel.OpenResultCommand, "7"),
new(RegisteredHotkeyType.OpenResultN9, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D9", nameof(Localize.HotkeyOpenResultN), 9, _mainViewModel.OpenResultCommand, "8"),
new(RegisteredHotkeyType.OpenResultN10, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D0", nameof(Localize.HotkeyOpenResultN), 10, _mainViewModel.OpenResultCommand, "9"),
// Flow Launcher global hotkeys
new(RegisteredHotkeyType.Toggle, HotkeyType.Global, _settings.Hotkey, nameof(Localize.flowlauncherHotkey), _mainViewModel.CheckAndToggleFlowLauncherCommand, null, () => _settings.Hotkey = ""),
new(RegisteredHotkeyType.DialogJump, HotkeyType.Global, _settings.DialogJumpHotkey, nameof(Localize.dialogJumpHotkey), DialogJump.DialogJumpCommand, null, () => _settings.DialogJumpHotkey = ""),
// Flow Launcher window hotkeys
new(RegisteredHotkeyType.Preview, HotkeyType.SearchWindow, _settings.PreviewHotkey, nameof(Localize.previewHotkey), _mainViewModel.TogglePreviewCommand, null, () => _settings.PreviewHotkey = ""),
new(RegisteredHotkeyType.AutoComplete, HotkeyType.SearchWindow, _settings.AutoCompleteHotkey, nameof(Localize.autoCompleteHotkey), _mainViewModel.AutocompleteQueryCommand, null, () => _settings.AutoCompleteHotkey = ""),
new(RegisteredHotkeyType.AutoComplete2, HotkeyType.SearchWindow, _settings.AutoCompleteHotkey2, nameof(Localize.autoCompleteHotkey), _mainViewModel.AutocompleteQueryCommand, null, () => _settings.AutoCompleteHotkey2 = ""),
new(RegisteredHotkeyType.SelectNextItem, HotkeyType.SearchWindow, _settings.SelectNextItemHotkey, nameof(Localize.SelectNextItemHotkey), _mainViewModel.SelectNextItemCommand, null, () => _settings.SelectNextItemHotkey = ""),
new(RegisteredHotkeyType.SelectNextItem2, HotkeyType.SearchWindow, _settings.SelectNextItemHotkey2, nameof(Localize.SelectNextItemHotkey), _mainViewModel.SelectNextItemCommand, null, () => _settings.SelectNextItemHotkey2 = ""),
new(RegisteredHotkeyType.SelectPrevItem, HotkeyType.SearchWindow, _settings.SelectPrevItemHotkey, nameof(Localize.SelectPrevItemHotkey), _mainViewModel.SelectPrevItemCommand, null, () => _settings.SelectPrevItemHotkey = ""),
new(RegisteredHotkeyType.SelectPrevItem2, HotkeyType.SearchWindow, _settings.SelectPrevItemHotkey2, nameof(Localize.SelectPrevItemHotkey), _mainViewModel.SelectPrevItemCommand, null, () => _settings.SelectPrevItemHotkey2 = ""),
new(RegisteredHotkeyType.SettingWindow, HotkeyType.SearchWindow, _settings.SettingWindowHotkey, nameof(Localize.SettingWindowHotkey), _mainViewModel.OpenSettingCommand, null, () => _settings.SettingWindowHotkey = ""),
new(RegisteredHotkeyType.OpenHistory, HotkeyType.SearchWindow, _settings.OpenHistoryHotkey, nameof(Localize.ToggleHistoryHotkey), _mainViewModel.LoadHistoryCommand, null, () => _settings.OpenHistoryHotkey = ""),
new(RegisteredHotkeyType.OpenContextMenu, HotkeyType.SearchWindow, _settings.OpenContextMenuHotkey, nameof(Localize.OpenContextMenuHotkey), _mainViewModel.LoadContextMenuCommand, null, () => _settings.OpenContextMenuHotkey = ""),
new(RegisteredHotkeyType.SelectNextPage, HotkeyType.SearchWindow, _settings.SelectNextPageHotkey, nameof(Localize.SelectNextPageHotkey), _mainViewModel.SelectNextPageCommand, null, () => _settings.SelectNextPageHotkey = ""),
new(RegisteredHotkeyType.SelectPrevPage, HotkeyType.SearchWindow, _settings.SelectPrevPageHotkey, nameof(Localize.SelectPrevPageHotkey), _mainViewModel.SelectPrevPageCommand, null, () => _settings.SelectPrevPageHotkey = ""),
new(RegisteredHotkeyType.CycleHistoryUp, HotkeyType.SearchWindow, _settings.CycleHistoryUpHotkey, nameof(Localize.CycleHistoryUpHotkey), _mainViewModel.ReverseHistoryCommand, null, () => _settings.CycleHistoryUpHotkey = ""),
new(RegisteredHotkeyType.CycleHistoryDown, HotkeyType.SearchWindow, _settings.CycleHistoryDownHotkey, nameof(Localize.CycleHistoryDownHotkey), _mainViewModel.ForwardHistoryCommand, null, () => _settings.CycleHistoryDownHotkey = "")
};
// Custom query global hotkeys
foreach (var customPluginHotkey in _settings.CustomPluginHotkeys)
{
list.Add(GetRegisteredHotkeyData(customPluginHotkey));
}
LoadCustomPluginHotkey();
// Add registered hotkeys & Set them
foreach (var hotkey in list)
{
_settings.RegisteredHotkeys.Add(hotkey);
if (hotkey.RegisteredType == RegisteredHotkeyType.DialogJump && !_settings.EnableDialogJump)
{
// If dialog jump is disabled, do not register the hotkey
continue;
}
SetHotkey(hotkey);
}
App.API.LogDebug(ClassName, $"Initialize {_settings.RegisteredHotkeys.Count} hotkeys:\n[\n\t{string.Join(",\n\t", _settings.RegisteredHotkeys)}\n]");
}
internal static void OnToggleHotkey(object sender, HotkeyEventArgs args)
private static void PluginManager_PluginHotkeyInitialized(PluginPair pair)
{
if (!_mainViewModel.ShouldIgnoreHotkeys())
_mainViewModel.ToggleFlowLauncher();
var list = new List<RegisteredHotkeyData>();
// Global plugin hotkeys
var pluginHotkeyInfos = PluginManager.GetPluginHotkeyInfo(pair.Metadata.ID);
foreach (var info in pluginHotkeyInfos)
{
var pluginPair = info.Key;
var hotkeyInfo = info.Value;
var metadata = pluginPair.Metadata;
foreach (var hotkey in hotkeyInfo)
{
if (hotkey.HotkeyType == HotkeyType.Global && hotkey is GlobalPluginHotkey globalHotkey)
{
var hotkeyStr = metadata.PluginHotkeys.Find(h => h.Id == hotkey.Id)?.Hotkey ?? hotkey.DefaultHotkey;
list.Add(GetRegisteredHotkeyData(new(hotkeyStr), metadata, globalHotkey));
}
}
}
// Window plugin hotkeys
var windowPluginHotkeys = PluginManager.GetWindowPluginHotkeys(pair.Metadata.ID);
foreach (var hotkey in windowPluginHotkeys)
{
var hotkeyModel = hotkey.Key;
var windowHotkeys = hotkey.Value;
list.Add(GetRegisteredHotkeyData(hotkeyModel, windowHotkeys));
}
// Add registered hotkeys & Set them
foreach (var hotkey in list)
{
_settings.RegisteredHotkeys.Add(hotkey);
SetHotkey(hotkey);
}
App.API.LogDebug(ClassName, $"Initialize {list.Count} hotkeys for {pair.Metadata.Name}:\n[\n\t{string.Join(",\n\t", list)}\n]");
}
internal static void OnToggleHotkeyWithChefKeys()
#endregion
#region Hotkey Change Events
private static void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (!_mainViewModel.ShouldIgnoreHotkeys())
_mainViewModel.ToggleFlowLauncher();
switch (e.PropertyName)
{
// Flow Launcher global hotkeys
case nameof(_settings.Hotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.Toggle, _settings.Hotkey);
break;
case nameof(_settings.DialogJumpHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.DialogJump, _settings.DialogJumpHotkey, _settings.EnableDialogJump);
break;
case nameof(_settings.EnableDialogJump):
ChangeRegisteredHotkey(RegisteredHotkeyType.DialogJump, _settings.DialogJumpHotkey, _settings.EnableDialogJump);
break;
// Flow Launcher window hotkeys
case nameof(_settings.PreviewHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.Preview, _settings.PreviewHotkey);
break;
case nameof(_settings.AutoCompleteHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.AutoComplete, _settings.AutoCompleteHotkey);
break;
case nameof(_settings.AutoCompleteHotkey2):
ChangeRegisteredHotkey(RegisteredHotkeyType.AutoComplete2, _settings.AutoCompleteHotkey2);
break;
case nameof(_settings.SelectNextItemHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.SelectNextItem, _settings.SelectNextItemHotkey);
break;
case nameof(_settings.SelectNextItemHotkey2):
ChangeRegisteredHotkey(RegisteredHotkeyType.SelectNextItem2, _settings.SelectNextItemHotkey2);
break;
case nameof(_settings.SelectPrevItemHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.SelectPrevItem, _settings.SelectPrevItemHotkey);
break;
case nameof(_settings.SelectPrevItemHotkey2):
ChangeRegisteredHotkey(RegisteredHotkeyType.SelectPrevItem2, _settings.SelectPrevItemHotkey2);
break;
case nameof(_settings.SettingWindowHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.SettingWindow, _settings.SettingWindowHotkey);
break;
case nameof(_settings.OpenHistoryHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenHistory, _settings.OpenHistoryHotkey);
break;
case nameof(_settings.OpenContextMenuHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenContextMenu, _settings.OpenContextMenuHotkey);
break;
case nameof(_settings.SelectNextPageHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.SelectNextPage, _settings.SelectNextPageHotkey);
break;
case nameof(_settings.SelectPrevPageHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.SelectPrevPage, _settings.SelectPrevPageHotkey);
break;
case nameof(_settings.CycleHistoryUpHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.CycleHistoryUp, _settings.CycleHistoryUpHotkey);
break;
case nameof(_settings.CycleHistoryDownHotkey):
ChangeRegisteredHotkey(RegisteredHotkeyType.CycleHistoryDown, _settings.CycleHistoryDownHotkey);
break;
// Result Modifier Hotkeys
case nameof(_settings.OpenResultModifiers):
// Change all result modifier hotkeys
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN1, $"{_settings.OpenResultModifiers}+D1");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN2, $"{_settings.OpenResultModifiers}+D2");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN3, $"{_settings.OpenResultModifiers}+D3");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN4, $"{_settings.OpenResultModifiers}+D4");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN5, $"{_settings.OpenResultModifiers}+D5");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN6, $"{_settings.OpenResultModifiers}+D6");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN7, $"{_settings.OpenResultModifiers}+D7");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN8, $"{_settings.OpenResultModifiers}+D8");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN9, $"{_settings.OpenResultModifiers}+D9");
ChangeRegisteredHotkey(RegisteredHotkeyType.OpenResultN10, $"{_settings.OpenResultModifiers}+D0");
break;
}
}
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
private static void CustomPluginHotkeys_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var hotkey = new HotkeyModel(hotkeyStr);
SetHotkey(hotkey, action);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var item in e.NewItems)
{
if (item is CustomPluginHotkey customPluginHotkey)
{
var hotkeyData = GetRegisteredHotkeyData(customPluginHotkey);
_settings.RegisteredHotkeys.Add(hotkeyData);
SetHotkey(hotkeyData);
}
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (var item in e.OldItems)
{
if (item is CustomPluginHotkey customPluginHotkey)
{
var hotkeyData = SearchRegisteredHotkeyData(customPluginHotkey);
_settings.RegisteredHotkeys.Remove(hotkeyData);
RemoveHotkey(hotkeyData);
}
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (var item in e.OldItems)
{
if (item is CustomPluginHotkey customPluginHotkey)
{
var hotkeyData = SearchRegisteredHotkeyData(customPluginHotkey);
_settings.RegisteredHotkeys.Remove(hotkeyData);
RemoveHotkey(hotkeyData);
}
}
foreach (var item in e.NewItems)
{
if (item is CustomPluginHotkey customPluginHotkey)
{
var hotkeyData = GetRegisteredHotkeyData(customPluginHotkey);
_settings.RegisteredHotkeys.Add(hotkeyData);
SetHotkey(hotkeyData);
}
}
break;
}
}
private static void SetWithChefKeys(string hotkeyStr)
private static void PluginManager_PluginHotkeyChanged(PluginManager.PluginHotkeyChangedEvent e)
{
var oldHotkey = e.OldHotkey;
var newHotkey = e.NewHotkey;
var metadata = e.Metadata;
var pluginHotkey = e.PluginHotkey;
if (pluginHotkey is GlobalPluginHotkey globalPluginHotkey)
{
var hotkeyData = SearchRegisteredHotkeyData(metadata, globalPluginHotkey);
RemoveHotkey(hotkeyData);
hotkeyData.SetHotkey(newHotkey);
SetHotkey(hotkeyData);
}
else if (pluginHotkey is SearchWindowPluginHotkey)
{
// Search hotkey & Remove registered hotkey data & Unregister hotkeys
var oldHotkeyData = SearchRegisteredHotkeyData(RegisteredHotkeyType.PluginWindowHotkey, oldHotkey);
_settings.RegisteredHotkeys.Remove(oldHotkeyData);
RemoveHotkey(oldHotkeyData);
var newHotkeyData = SearchRegisteredHotkeyData(RegisteredHotkeyType.PluginWindowHotkey, newHotkey);
_settings.RegisteredHotkeys.Remove(newHotkeyData);
RemoveHotkey(newHotkeyData);
// Get hotkey data & Add new registered hotkeys & Register hotkeys
var windowPluginHotkeys = PluginManager.GetWindowPluginHotkeys();
if (windowPluginHotkeys.TryGetValue(oldHotkey, out var oldHotkeyModels))
{
oldHotkeyData = GetRegisteredHotkeyData(oldHotkey, oldHotkeyModels);
_settings.RegisteredHotkeys.Add(oldHotkeyData);
SetHotkey(oldHotkeyData);
}
if (windowPluginHotkeys.TryGetValue(newHotkey, out var newHotkeyModels))
{
newHotkeyData = GetRegisteredHotkeyData(newHotkey, newHotkeyModels);
_settings.RegisteredHotkeys.Add(newHotkeyData);
SetHotkey(newHotkeyData);
}
}
}
#endregion
#region Custom Query Hotkey
private static RegisteredHotkeyData GetRegisteredHotkeyData(CustomPluginHotkey customPluginHotkey)
{
return new(RegisteredHotkeyType.CustomQuery, HotkeyType.Global, customPluginHotkey.Hotkey, "customQueryHotkey", CustomQueryHotkeyCommand, customPluginHotkey, () => ClearHotkeyForCustomQueryHotkey(customPluginHotkey));
}
private static RegisteredHotkeyData SearchRegisteredHotkeyData(CustomPluginHotkey customPluginHotkey)
{
return _settings.RegisteredHotkeys.FirstOrDefault(h =>
h.RegisteredType == RegisteredHotkeyType.CustomQuery &&
customPluginHotkey.Equals(h.CommandParameter));
}
private static void ClearHotkeyForCustomQueryHotkey(CustomPluginHotkey customPluginHotkey)
{
// Clear hotkey for custom query hotkey
customPluginHotkey.Hotkey = string.Empty;
// Remove hotkey events
var hotkeyData = SearchRegisteredHotkeyData(customPluginHotkey);
_settings.RegisteredHotkeys.Remove(hotkeyData);
RemoveHotkey(hotkeyData);
}
#endregion
#region Plugin Hotkey
private static RegisteredHotkeyData GetRegisteredHotkeyData(HotkeyModel hotkey, PluginMetadata metadata, GlobalPluginHotkey pluginHotkey)
{
Action removeHotkeyAction = pluginHotkey.Editable ?
() => PluginManager.ChangePluginHotkey(metadata, pluginHotkey, HotkeyModel.Empty) : null;
return new(RegisteredHotkeyType.PluginGlobalHotkey, HotkeyType.Global, hotkey, "pluginHotkey", GlobalPluginHotkeyCommand, new GlobalPluginHotkeyPair(metadata, pluginHotkey), removeHotkeyAction);
}
private static RegisteredHotkeyData GetRegisteredHotkeyData(HotkeyModel hotkey, ConcurrentBag<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> windowHotkeys)
{
Action removeHotkeysAction = windowHotkeys.All(h => h.PluginHotkey.Editable) ?
() =>
{
foreach (var (metadata, pluginHotkey) in windowHotkeys)
{
PluginManager.ChangePluginHotkey(metadata, pluginHotkey, HotkeyModel.Empty);
}
} : null;
return new(RegisteredHotkeyType.PluginWindowHotkey, HotkeyType.SearchWindow, hotkey, "pluginHotkey", WindowPluginHotkeyCommand, new WindowPluginHotkeyPair(hotkey, windowHotkeys), removeHotkeysAction);
}
private static RegisteredHotkeyData SearchRegisteredHotkeyData(PluginMetadata metadata, GlobalPluginHotkey globalPluginHotkey)
{
return _settings.RegisteredHotkeys.FirstOrDefault(h =>
h.RegisteredType == RegisteredHotkeyType.PluginGlobalHotkey &&
h.CommandParameter is GlobalPluginHotkeyPair pair &&
pair.Metadata.ID == metadata.ID &&
pair.GlobalPluginHotkey.Id == globalPluginHotkey.Id);
}
#endregion
#region Hotkey Setting
private static void SetHotkey(RegisteredHotkeyData hotkeyData)
{
if (hotkeyData is null || // Hotkey data is invalid
hotkeyData.Hotkey.IsEmpty || // Hotkey is none
hotkeyData.Command is null) // No need to set - it is a system command
{
return;
}
if (hotkeyData.Type == HotkeyType.Global)
{
SetGlobalHotkey(hotkeyData);
}
else if (hotkeyData.Type == HotkeyType.SearchWindow)
{
SetWindowHotkey(hotkeyData);
}
}
private static void RemoveHotkey(RegisteredHotkeyData hotkeyData)
{
if (hotkeyData is null || // Hotkey data is invalid
hotkeyData.Hotkey.IsEmpty) // Hotkey is none
{
return;
}
if (hotkeyData.Type == HotkeyType.Global)
{
RemoveGlobalHotkey(hotkeyData);
}
else if (hotkeyData.Type == HotkeyType.SearchWindow)
{
RemoveWindowHotkey(hotkeyData);
}
}
private static void SetGlobalHotkey(RegisteredHotkeyData hotkeyData)
{
var hotkey = hotkeyData.Hotkey;
var hotkeyStr = hotkey.ToString();
var hotkeyCommand = hotkeyData.Command;
var hotkeyCommandParameter = hotkeyData.CommandParameter;
try
{
ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, OnToggleHotkeyWithChefKeys);
if (hotkeyStr == "LWin" || hotkeyStr == "RWin")
{
SetGlobalHotkeyWithChefKeys(hotkeyData);
return;
}
HotkeyManager.Current.AddOrReplace(
hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys,
(s, e) => hotkeyCommand.Execute(hotkeyCommandParameter));
}
catch (Exception e)
{
App.API.LogError(ClassName, $"Error registering hotkey {hotkeyStr}: {e.Message} \nStackTrace:{e.StackTrace}");
var errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
var errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
private static void SetGlobalHotkeyWithChefKeys(RegisteredHotkeyData hotkeyData)
{
var hotkey = hotkeyData.Hotkey;
if (hotkey.IsEmpty)
{
return;
}
var hotkeyStr = hotkey.ToString();
var hotkeyCommand = hotkeyData.Command;
var hotkeyCommandParameter = hotkeyData.CommandParameter;
try
{
ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, () => hotkeyCommand.Execute(hotkeyCommandParameter));
ChefKeysManager.Start();
}
catch (Exception e)
{
App.API.LogError(ClassName,
string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
string.Format("Error registering hotkey: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace));
string errorMsg = Localize.registerHotkeyFailed(hotkeyStr);
@ -67,39 +492,60 @@ internal static class HotKeyMapper
}
}
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
private static void SetWindowHotkey(RegisteredHotkeyData hotkeyData)
{
string hotkeyStr = hotkey.ToString();
var hotkey = hotkeyData.Hotkey;
var hotkeyCommand = hotkeyData.Command;
var hotkeyCommandParameter = hotkeyData.CommandParameter;
try
{
if (hotkeyStr == "LWin" || hotkeyStr == "RWin")
if (Application.Current?.MainWindow is MainWindow window)
{
SetWithChefKeys(hotkeyStr);
return;
}
// Check if the hotkey already exists
var keyGesture = hotkey.ToKeyGesture();
var existingBinding = window.InputBindings
.OfType<KeyBinding>()
.FirstOrDefault(kb =>
kb.Gesture is KeyGesture keyGesture1 &&
keyGesture.Key == keyGesture1.Key &&
keyGesture.Modifiers == keyGesture1.Modifiers);
if (existingBinding != null)
{
// If the hotkey is not a hotkey for ActionContext events, throw an exception to avoid duplicates
if (!IsActionContextEvent(existingBinding, hotkey))
{
throw new InvalidOperationException($"Key {hotkey} already exists in window");
}
}
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
// Add the new hotkey binding
var keyBinding = new KeyBinding()
{
Gesture = keyGesture,
Command = hotkeyCommand,
CommandParameter = hotkeyCommandParameter
};
window.InputBindings.Add(keyBinding);
}
}
catch (Exception e)
{
App.API.LogError(ClassName,
string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace,
hotkeyStr));
string errorMsg = Localize.registerHotkeyFailed(hotkeyStr);
string errorMsgTitle = Localize.MessageBoxTitle();
App.API.LogError(ClassName, $"Error registering window hotkey {hotkey}: {e.Message} \nStackTrace:{e.StackTrace}");
var errorMsg = Localize.registerHotkeyFailed(hotkey);
var errorMsgTitle = Localize.MessageBoxTitle();
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
internal static void RemoveHotkey(string hotkeyStr)
private static void RemoveGlobalHotkey(RegisteredHotkeyData hotkeyData)
{
var hotkey = hotkeyData.Hotkey;
var hotkeyStr = hotkey.ToString();
try
{
if (hotkeyStr == "LWin" || hotkeyStr == "RWin")
{
RemoveWithChefKeys(hotkeyStr);
RemoveGlobalHotkeyWithChefKeys(hotkeyData);
return;
}
@ -108,47 +554,183 @@ internal static class HotKeyMapper
}
catch (Exception e)
{
App.API.LogError(ClassName,
string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace));
string errorMsg = Localize.unregisterHotkeyFailed(hotkeyStr);
string errorMsgTitle = Localize.MessageBoxTitle();
App.API.LogError(ClassName, $"Error removing hotkey: {e.Message} \nStackTrace:{e.StackTrace}");
var errorMsg = Localize.unregisterHotkeyFailed(hotkeyStr);
var errorMsgTitle = Localize.MessageBoxTitle();
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
private static void RemoveWithChefKeys(string hotkeyStr)
private static void RemoveGlobalHotkeyWithChefKeys(RegisteredHotkeyData hotkeyData)
{
ChefKeysManager.UnregisterHotkey(hotkeyStr);
ChefKeysManager.Stop();
}
internal static void LoadCustomPluginHotkey()
{
if (_settings.CustomPluginHotkeys == null)
return;
foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys)
var hotkey = hotkeyData.Hotkey;
var hotkeyStr = hotkey.ToString();
try
{
SetCustomQueryHotkey(hotkey);
ChefKeysManager.UnregisterHotkey(hotkeyStr);
ChefKeysManager.Stop();
}
catch (Exception e)
{
App.API.LogError(ClassName, $"Error removing hotkey: {e.Message} \nStackTrace:{e.StackTrace}");
var errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr);
var errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey)
private static void RemoveWindowHotkey(RegisteredHotkeyData hotkeyData)
{
SetHotkey(hotkey.Hotkey, (s, e) =>
var hotkey = hotkeyData.Hotkey;
try
{
if (_mainViewModel.ShouldIgnoreHotkeys())
return;
if (Application.Current?.MainWindow is MainWindow window)
{
// Remove the key binding
var keyGesture = hotkey.ToKeyGesture();
var existingBinding = window.InputBindings
.OfType<KeyBinding>()
.FirstOrDefault(kb =>
kb.Gesture is KeyGesture keyGesture1 &&
keyGesture.Key == keyGesture1.Key &&
keyGesture.Modifiers == keyGesture1.Modifiers);
if (existingBinding != null)
{
window.InputBindings.Remove(existingBinding);
}
App.API.ShowMainWindow();
// Make sure to go back to the query results page first since it can cause issues if current page is context menu
App.API.BackToQueryResults();
App.API.ChangeQuery(hotkey.ActionKeyword, true);
});
// Restore the key binding for ActionContext events
RestoreActionContextEvent(hotkey, keyGesture);
}
}
catch (Exception e)
{
App.API.LogError(ClassName, $"Error removing window hotkey: {e.Message} \nStackTrace:{e.StackTrace}");
var errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkey);
var errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
#endregion
#region Hotkey Changing
private static void ChangeRegisteredHotkey(RegisteredHotkeyType registeredType, string newHotkeyStr, bool setHotkey = true)
{
var newHotkey = new HotkeyModel(newHotkeyStr);
ChangeRegisteredHotkey(registeredType, newHotkey, setHotkey);
}
private static void ChangeRegisteredHotkey(RegisteredHotkeyType registeredType, HotkeyModel newHotkey, bool setHotkey = true)
{
// Find the old registered hotkey data item
var registeredHotkeyData = _settings.RegisteredHotkeys.FirstOrDefault(h => h.RegisteredType == registeredType);
// If it is not found, return
if (registeredHotkeyData == null) return;
// Remove the old hotkey
RemoveHotkey(registeredHotkeyData);
// Update the hotkey string
registeredHotkeyData.SetHotkey(newHotkey);
// Set the new hotkey
if (setHotkey)
{
SetHotkey(registeredHotkeyData);
}
}
#endregion
#region Hotkey Searching
private static RegisteredHotkeyData SearchRegisteredHotkeyData(RegisteredHotkeyType registeredHotkeyType, HotkeyModel hotkeyModel)
{
return _settings.RegisteredHotkeys.FirstOrDefault(h =>
h.RegisteredType == registeredHotkeyType &&
h.Hotkey.Equals(hotkeyModel));
}
#endregion
#region Commands
private static RelayCommand<CustomPluginHotkey> _customQueryHotkeyCommand;
private static IRelayCommand CustomQueryHotkeyCommand => _customQueryHotkeyCommand ??= new RelayCommand<CustomPluginHotkey>(CustomQueryHotkey);
private static RelayCommand<GlobalPluginHotkeyPair> _globalPluginHotkeyCommand;
private static IRelayCommand GlobalPluginHotkeyCommand => _globalPluginHotkeyCommand ??= new RelayCommand<GlobalPluginHotkeyPair>(GlobalPluginHotkey);
private static RelayCommand<WindowPluginHotkeyPair> _windowPluginHotkeyCommand;
private static IRelayCommand WindowPluginHotkeyCommand => _windowPluginHotkeyCommand ??= new RelayCommand<WindowPluginHotkeyPair>(WindowPluginHotkey);
private static void CustomQueryHotkey(CustomPluginHotkey customPluginHotkey)
{
if (_mainViewModel.ShouldIgnoreHotkeys())
return;
App.API.ShowMainWindow();
// Make sure to go back to the query results page first since it can cause issues if current page is context menu
App.API.BackToQueryResults();
App.API.ChangeQuery(customPluginHotkey.ActionKeyword, true);
}
private static void GlobalPluginHotkey(GlobalPluginHotkeyPair pair)
{
var metadata = pair.Metadata;
var pluginHotkey = pair.GlobalPluginHotkey;
if (metadata.Disabled || // Check plugin enabled state
App.API.PluginModified(metadata.ID)) // Check plugin modified state
return;
if (_mainViewModel.ShouldIgnoreHotkeys())
return;
pluginHotkey.Action?.Invoke();
}
private static void WindowPluginHotkey(WindowPluginHotkeyPair pair)
{
// Get selected result
var selectedResult = _mainViewModel.GetSelectedResults().SelectedItem?.Result;
// Check result nullability
if (selectedResult != null)
{
var pluginId = selectedResult.PluginID;
foreach (var hotkeyModel in pair.HotkeyModels)
{
var metadata = hotkeyModel.Metadata;
var pluginHotkey = hotkeyModel.PluginHotkey;
if (metadata.ID != pluginId || // Check plugin ID match
metadata.Disabled || // Check plugin enabled state
App.API.PluginModified(metadata.ID) || // Check plugin modified state
!selectedResult.HotkeyIds.Contains(pluginHotkey.Id) || // Check hotkey supported state
pluginHotkey.Action == null) // Check action nullability
continue;
// TODO: Remove return to skip other commands
if (pluginHotkey.Action.Invoke(selectedResult))
App.API.HideMainWindow();
// Return after invoking the first matching hotkey action so that we will not invoke action context event
return;
}
// When no plugin hotkey action is invoked, invoke the action context event
InvokeActionContextEvent(pair.Hotkey);
}
}
#endregion
#region Check Hotkey
internal static bool CheckAvailability(HotkeyModel currentHotkey)
{
try
@ -167,4 +749,103 @@ internal static class HotKeyMapper
return false;
}
#endregion
#region Action Context Hotkey (Obsolete)
[Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")]
private static List<RegisteredHotkeyData> _actionContextRegisteredHotkeys;
[Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")]
private static readonly Dictionary<HotkeyModel, (ICommand Command, object Parameter)> _actionContextHotkeyEvents = new();
[Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")]
private static void InitializeActionContextHotkeys()
{
// Fixed hotkeys for ActionContext
_actionContextRegisteredHotkeys =
[
new(RegisteredHotkeyType.CtrlShiftEnter, HotkeyType.SearchWindow, "Ctrl+Shift+Enter", nameof(Localize.HotkeyCtrlShiftEnterDesc), _mainViewModel.OpenResultCommand),
new(RegisteredHotkeyType.CtrlEnter, HotkeyType.SearchWindow, "Ctrl+Enter", nameof(Localize.OpenContainFolderHotkey), _mainViewModel.OpenResultCommand),
new(RegisteredHotkeyType.AltEnter, HotkeyType.SearchWindow, "Alt+Enter", nameof(Localize.HotkeyOpenResult), _mainViewModel.OpenResultCommand),
];
// Register ActionContext hotkeys and they will be cached and restored in _actionContextHotkeyEvents
foreach (var hotkey in _actionContextRegisteredHotkeys)
{
_actionContextHotkeyEvents[hotkey.Hotkey] = (hotkey.Command, hotkey.CommandParameter);
SetWindowHotkey(hotkey);
}
}
[Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")]
private static bool IsActionContextEvent(KeyBinding existingBinding, HotkeyModel hotkey)
{
// Check if this hotkey is a hotkey for ActionContext events
if (_actionContextHotkeyEvents.TryGetValue(hotkey, out var value) &&
value.Command == existingBinding.Command &&
value.Parameter == existingBinding.CommandParameter)
{
// If the hotkey is not for ActionContext events, return false
return true;
}
return false;
}
[Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")]
private static void RestoreActionContextEvent(HotkeyModel hotkey, KeyGesture keyGesture)
{
// Restore the ActionContext event by adding the key binding back
if (_actionContextHotkeyEvents.TryGetValue(hotkey, out var actionContextItem))
{
if (Application.Current?.MainWindow is MainWindow window)
{
var keyBinding = new KeyBinding
{
Gesture = keyGesture,
Command = actionContextItem.Command,
CommandParameter = actionContextItem.Parameter
};
window.InputBindings.Add(keyBinding);
}
}
}
[Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")]
private static void InvokeActionContextEvent(HotkeyModel hotkey)
{
if (_actionContextHotkeyEvents.TryGetValue(hotkey, out var actionContextItem))
{
actionContextItem.Command.Execute(actionContextItem.Parameter);
}
}
#endregion
#region Private Classes
private class GlobalPluginHotkeyPair
{
public PluginMetadata Metadata { get; }
public GlobalPluginHotkey GlobalPluginHotkey { get; }
public GlobalPluginHotkeyPair(PluginMetadata metadata, GlobalPluginHotkey globalPluginHotkey)
{
Metadata = metadata;
GlobalPluginHotkey = globalPluginHotkey;
}
}
private class WindowPluginHotkeyPair(HotkeyModel hotkey, ConcurrentBag<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> hotkeys)
{
[Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")]
public HotkeyModel Hotkey { get; } = hotkey;
public ConcurrentBag<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> HotkeyModels { get; } = hotkeys;
}
#endregion
}

View file

@ -112,6 +112,9 @@ namespace Flow.Launcher
SelectNextItemHotkey,
SelectNextItemHotkey2,
DialogJumpHotkey,
// Plugin hotkeys
GlobalPluginHotkey,
WindowPluginHotkey,
}
// We can initialize settings in static field because it has been constructed in App constuctor
@ -144,6 +147,9 @@ namespace Flow.Launcher
HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey,
HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2,
HotkeyType.DialogJumpHotkey => _settings.DialogJumpHotkey,
// Plugin hotkeys
HotkeyType.GlobalPluginHotkey => hotkey,
HotkeyType.WindowPluginHotkey => hotkey,
_ => throw new System.NotImplementedException("Hotkey type not set")
};
}
@ -206,6 +212,17 @@ namespace Flow.Launcher
case HotkeyType.DialogJumpHotkey:
_settings.DialogJumpHotkey = value;
break;
// Plugin hotkeys
case HotkeyType.GlobalPluginHotkey:
// We should not save it to settings here because it is a custom plugin hotkey
// and it will be saved in the plugin settings
hotkey = value;
break;
case HotkeyType.WindowPluginHotkey:
// We should not save it to settings here because it is a custom plugin hotkey
// and it will be saved in the plugin settings
hotkey = value;
break;
default:
throw new System.NotImplementedException("Hotkey type not set");
}
@ -247,11 +264,6 @@ namespace Flow.Launcher
private async Task OpenHotkeyDialogAsync()
{
if (!string.IsNullOrEmpty(Hotkey))
{
HotKeyMapper.RemoveHotkey(Hotkey);
}
var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle)
{
Owner = Window.GetWindow(this)
@ -305,8 +317,6 @@ namespace Flow.Launcher
public void Delete()
{
if (!string.IsNullOrEmpty(Hotkey))
HotKeyMapper.RemoveHotkey(Hotkey);
Hotkey = "";
SetKeysToDisplay(new HotkeyModel(false, false, false, false, Key.None));
}

View file

@ -428,6 +428,7 @@
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Fill full path in file name box</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Fill full path in file name box and open</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Fill directory in path box</system:String>
<system:String x:Key="pluginHotkey">Plugin hotkey</system:String>
<!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP Proxy</system:String>

View file

@ -49,169 +49,6 @@
<converters:BoolToIMEStateConverter x:Key="BoolToIMEStateConverter" />
<converters:StringToKeyBindingConverter x:Key="StringToKeyBindingConverter" />
</Window.Resources>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="{Binding EscCommand}" />
<KeyBinding Key="F5" Command="{Binding ReloadPluginDataCommand}" />
<KeyBinding
Key="Home"
Command="{Binding SelectFirstResultCommand}"
Modifiers="Alt" />
<KeyBinding
Key="End"
Command="{Binding SelectLastResultCommand}"
Modifiers="Alt" />
<KeyBinding
Key="R"
Command="{Binding ReQueryCommand}"
Modifiers="Ctrl" />
<KeyBinding
Key="OemCloseBrackets"
Command="{Binding IncreaseWidthCommand}"
Modifiers="Control" />
<KeyBinding
Key="OemOpenBrackets"
Command="{Binding DecreaseWidthCommand}"
Modifiers="Control" />
<KeyBinding
Key="OemPlus"
Command="{Binding IncreaseMaxResultCommand}"
Modifiers="Control" />
<KeyBinding
Key="OemMinus"
Command="{Binding DecreaseMaxResultCommand}"
Modifiers="Control" />
<KeyBinding
Key="Enter"
Command="{Binding OpenResultCommand}"
Modifiers="Ctrl+Shift" />
<KeyBinding
Key="Enter"
Command="{Binding LoadContextMenuCommand}"
Modifiers="Shift" />
<KeyBinding Key="Enter" Command="{Binding OpenResultCommand}" />
<KeyBinding
Key="Enter"
Command="{Binding OpenResultCommand}"
Modifiers="Ctrl" />
<KeyBinding
Key="Enter"
Command="{Binding OpenResultCommand}"
Modifiers="Alt" />
<KeyBinding
Key="D1"
Command="{Binding OpenResultCommand}"
CommandParameter="0"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D2"
Command="{Binding OpenResultCommand}"
CommandParameter="1"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D3"
Command="{Binding OpenResultCommand}"
CommandParameter="2"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D4"
Command="{Binding OpenResultCommand}"
CommandParameter="3"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D5"
Command="{Binding OpenResultCommand}"
CommandParameter="4"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D6"
Command="{Binding OpenResultCommand}"
CommandParameter="5"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D7"
Command="{Binding OpenResultCommand}"
CommandParameter="6"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D8"
Command="{Binding OpenResultCommand}"
CommandParameter="7"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D9"
Command="{Binding OpenResultCommand}"
CommandParameter="8"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="D0"
Command="{Binding OpenResultCommand}"
CommandParameter="9"
Modifiers="{Binding OpenResultCommandModifiers}" />
<KeyBinding
Key="F12"
Command="{Binding ToggleGameModeCommand}"
Modifiers="Ctrl" />
<KeyBinding
Key="C"
Command="{Binding CopyAlternativeCommand}"
Modifiers="Ctrl+Shift" />
<KeyBinding
Key="{Binding PreviewHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding TogglePreviewCommand}"
Modifiers="{Binding PreviewHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding AutoCompleteHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding AutocompleteQueryCommand}"
Modifiers="{Binding AutoCompleteHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding AutoCompleteHotkey2, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding AutocompleteQueryCommand}"
Modifiers="{Binding AutoCompleteHotkey2, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding SelectNextItemHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding SelectNextItemCommand}"
Modifiers="{Binding SelectNextItemHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding SelectPrevItemHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding SelectPrevItemCommand}"
Modifiers="{Binding SelectPrevItemHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding SelectNextItemHotkey2, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding SelectNextItemCommand}"
Modifiers="{Binding SelectNextItemHotkey2, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding SelectPrevItemHotkey2, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding SelectPrevItemCommand}"
Modifiers="{Binding SelectPrevItemHotkey2, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding SettingWindowHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding OpenSettingCommand}"
Modifiers="{Binding SettingWindowHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding OpenHistoryHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding LoadHistoryCommand}"
Modifiers="{Binding OpenHistoryHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding OpenContextMenuHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding LoadContextMenuCommand}"
Modifiers="{Binding OpenContextMenuHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding SelectNextPageHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding SelectNextPageCommand}"
Modifiers="{Binding SelectNextPageHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding SelectPrevPageHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding SelectPrevPageCommand}"
Modifiers="{Binding SelectPrevPageHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding CycleHistoryUpHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding ReverseHistoryCommand}"
Modifiers="{Binding CycleHistoryUpHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
<KeyBinding
Key="{Binding CycleHistoryDownHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding ForwardHistoryCommand}"
Modifiers="{Binding CycleHistoryDownHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
</Window.InputBindings>
<Border MouseDown="OnMouseDown" Style="{DynamicResource WindowBorderStyle}">
<StackPanel Orientation="Vertical">

View file

@ -113,13 +113,11 @@
Text="{DynamicResource flowlauncherHotkey}" />
<flowlauncher:HotkeyControl
Margin="0 8 0 0"
ChangeHotkey="{Binding SetTogglingHotkeyCommand}"
DefaultHotkey="Alt+Space"
Type="Hotkey"
ValidateKeyGesture="True"
WindowTitle="{DynamicResource flowlauncherHotkey}" />
</StackPanel>
</Grid>
</Grid>
</ui:ScrollViewerEx>

View file

@ -1,9 +1,7 @@
using System.Windows.Media;
using System.Windows.Navigation;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.ViewModel;
@ -27,12 +25,6 @@ namespace Flow.Launcher.Resources.Pages
base.OnNavigatedTo(e);
}
[RelayCommand]
private static void SetTogglingHotkey(HotkeyModel hotkey)
{
HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey);
}
public Brush PreviewBackground
{
get => WallpaperPathRetrieval.GetWallpaperBrush();

View file

@ -90,7 +90,9 @@
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyRunDesc}">
<cc:HotkeyDisplay Keys="ENTER" Type="Small" />
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="ENTER" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
@ -100,7 +102,7 @@
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyShiftEnterDesc}">
Header="{DynamicResource OpenContextMenuHotkey}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="SHIFT+ENTER" Type="Small" />
</StackPanel>
@ -113,9 +115,9 @@
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyCtrlEnterDesc}">
Header="{DynamicResource ReloadPluginHotkey}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="CTRL+ENTER" Type="Small" />
<cc:HotkeyDisplay Keys="F5" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
@ -126,9 +128,95 @@
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyCtrlShiftEnterDesc}">
Header="{DynamicResource HotkeySelectFirstResult}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="CTRL+SHIFT+ENTER" Type="Small" />
<cc:HotkeyDisplay Keys="Alt+Home" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeySelectLastResult}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="Alt+End" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyRequery}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="Ctrl+R" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource ToggleGameModeHotkey}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="Ctrl+F12" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource CopyFilePathHotkey}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="Ctrl+Shift+C" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource QuickWidthHotkey}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="Ctrl+[" Type="Small" />
<cc:HotkeyDisplay
Margin="4 0 0 0"
Keys="Ctrl+]"
Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource QuickHeightHotkey}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="Ctrl+Minus" Type="Small" />
<cc:HotkeyDisplay
Margin="4 0 0 0"
Keys="Ctrl+Plus"
Type="Small" />
</StackPanel>
</ui:SettingsCard>
</StackPanel>

View file

@ -158,14 +158,6 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
{
Settings.EnableDialogJump = value;
DialogJump.SetupDialogJump(value);
if (Settings.EnableDialogJump)
{
HotKeyMapper.SetHotkey(new(Settings.DialogJumpHotkey), DialogJump.OnToggleHotkey);
}
else
{
HotKeyMapper.RemoveHotkey(Settings.DialogJumpHotkey);
}
}
}
}

View file

@ -1,10 +1,7 @@
using System.Linq;
using System.Windows;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -29,21 +26,6 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
Settings = settings;
}
[RelayCommand]
private void SetTogglingHotkey(HotkeyModel hotkey)
{
HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey);
}
[RelayCommand]
private void SetDialogJumpHotkey(HotkeyModel hotkey)
{
if (Settings.EnableDialogJump)
{
HotKeyMapper.SetHotkey(hotkey, DialogJump.OnToggleHotkey);
}
}
[RelayCommand]
private void CustomHotkeyDelete()
{
@ -63,7 +45,6 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
if (result is MessageBoxResult.Yes)
{
Settings.CustomPluginHotkeys.Remove(item);
HotKeyMapper.RemoveHotkey(item.Hotkey);
}
}
@ -92,8 +73,6 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
if (index >= 0 && index < Settings.CustomPluginHotkeys.Count)
{
Settings.CustomPluginHotkeys[index] = new CustomPluginHotkey(window.Hotkey, window.ActionKeyword);
HotKeyMapper.RemoveHotkey(settingItem.Hotkey); // remove origin hotkey
HotKeyMapper.SetCustomQueryHotkey(Settings.CustomPluginHotkeys[index]); // set new hotkey
}
}
@ -105,7 +84,6 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
{
var customHotkey = new CustomPluginHotkey(window.Hotkey, window.ActionKeyword);
Settings.CustomPluginHotkeys.Add(customHotkey);
HotKeyMapper.SetCustomQueryHotkey(customHotkey); // set new hotkey
}
}

View file

@ -37,7 +37,6 @@
</ui:SettingsCard.HeaderIcon>
<flowlauncher:HotkeyControl
ChangeHotkey="{Binding SetTogglingHotkeyCommand}"
DefaultHotkey="Alt+Space"
Type="Hotkey"
ValidateKeyGesture="True"
@ -89,7 +88,6 @@
</ui:SettingsCard.HeaderIcon>
<flowlauncher:HotkeyControl
ChangeHotkey="{Binding SetDialogJumpHotkeyCommand}"
DefaultHotkey="Alt+G"
Type="DialogJumpHotkey"
ValidateKeyGesture="False"
@ -105,36 +103,40 @@
</ui:SettingsExpander.HeaderIcon>
<ui:SettingsExpander.Items>
<ui:SettingsCard Header="{DynamicResource OpenContainFolderHotkey}">
<ui:SettingsCard Header="{DynamicResource HotkeyUpDownDesc}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe8b7;" />
<ui:FontIcon Glyph="&#xede3;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Ctrl+Enter" />
<cc:HotkeyDisplay Keys="←+→" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource RunAsAdminHotkey}">
<ui:SettingsCard Header="{DynamicResource HotkeyLeftRightDesc}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe7ef;" />
<ui:FontIcon Glyph="&#xEC8F;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Ctrl+Shift+Enter" />
<cc:HotkeyDisplay Keys="↑+↓" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource ToggleHistoryHotkey}">
<ui:SettingsCard Header="{DynamicResource HotkeyESCDesc}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xf738;" />
<ui:FontIcon Glyph="&#xE72B;" />
</ui:SettingsCard.HeaderIcon>
<flowlauncher:HotkeyControl
DefaultHotkey="Ctrl+H"
Type="OpenHistoryHotkey"
ValidateKeyGesture="False" />
<cc:HotkeyDisplay Keys="ESC" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource CopyFilePathHotkey}">
<ui:SettingsCard Header="{DynamicResource HotkeyRunDesc}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe8c8;" />
<ui:FontIcon Glyph="&#xE8A7;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Ctrl+Shift+C" />
<cc:HotkeyDisplay Keys="ENTER" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource OpenContextMenuHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xede3;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Shift+Enter" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource OpenContextMenuHotkey}">
<ui:SettingsCard.HeaderIcon>
@ -146,19 +148,15 @@
Type="OpenContextMenuHotkey"
ValidateKeyGesture="False" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource OpenContextMenuHotkey}">
<ui:SettingsCard Header="{DynamicResource ToggleHistoryHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xede3;" />
<ui:FontIcon Glyph="&#xf738;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Shift+Enter" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource OpenNativeContextMenuHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xede3;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Alt+Enter" />
<flowlauncher:HotkeyControl
DefaultHotkey="Ctrl+H"
Type="OpenHistoryHotkey"
ValidateKeyGesture="False" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource SettingWindowHotkey}">
<ui:SettingsCard.HeaderIcon>
@ -170,19 +168,26 @@
Type="SettingWindowHotkey"
ValidateKeyGesture="False" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource ToggleGameModeHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe7fc;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Ctrl+F12" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource RequeryHotkey}">
<ui:SettingsCard Description="{DynamicResource ReloadPluginHotkeyToolTip}" Header="{DynamicResource ReloadPluginHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe72c;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Ctrl+R" />
<cc:HotkeyDisplay Keys="F5" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource HotkeySelectFirstResult}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xE898;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Alt+Home" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource HotkeySelectLastResult}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xE896;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Alt+End" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource CycleHistoryUpHotkey}">
<ui:SettingsCard.HeaderIcon>
@ -204,13 +209,6 @@
Type="CycleHistoryDownHotkey"
ValidateKeyGesture="False" />
</ui:SettingsCard>
<ui:SettingsCard Description="{DynamicResource ReloadPluginHotkeyToolTip}" Header="{DynamicResource ReloadPluginHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe72c;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="F5" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource SelectPrevPageHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xf0ad;" />
@ -231,7 +229,27 @@
Type="SelectNextPageHotkey"
ValidateKeyGesture="False" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource RequeryHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe72c;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Ctrl+R" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource ToggleGameModeHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe7fc;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Ctrl+F12" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource CopyFilePathHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe8c8;" />
</ui:SettingsCard.HeaderIcon>
<cc:HotkeyDisplay Keys="Ctrl+Shift+C" />
</ui:SettingsCard>
<ui:SettingsCard Header="{DynamicResource QuickWidthHotkey}">
<ui:SettingsCard.HeaderIcon>
<ui:FontIcon Glyph="&#xe7ea;" />
@ -248,8 +266,8 @@
</ui:SettingsCard.HeaderIcon>
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="Ctrl+Plus" />
<cc:HotkeyDisplay Margin="4 0 0 0" Keys="Ctrl+Minus" />
<cc:HotkeyDisplay Keys="Ctrl+Minus" />
<cc:HotkeyDisplay Margin="4 0 0 0" Keys="Ctrl+Plus" />
</StackPanel>
</ui:SettingsCard>
</ui:SettingsExpander.Items>
@ -318,6 +336,12 @@
</ui:SettingsExpander.Items>
</ui:SettingsExpander>
<StackPanel
x:Name="PluginHotkeySettings"
Margin="0 10 0 0"
Loaded="PluginHotkeySettings_Loaded"
Orientation="Vertical" />
<ui:SettingsExpander Margin="0 20 0 0" Header="{DynamicResource customQueryHotkey}">
<ui:SettingsExpander.HeaderIcon>
<ui:FontIcon Glyph="&#xf26c;" />

View file

@ -1,7 +1,19 @@
using System.Windows.Navigation;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Plugin;
using Flow.Launcher.Resources.Controls;
using Flow.Launcher.SettingPages.ViewModels;
using Flow.Launcher.ViewModel;
using iNKORE.UI.WPF.Modern.Controls;
using System.Threading.Tasks;
using System.Windows.Media;
namespace Flow.Launcher.SettingPages.Views;
@ -28,4 +40,129 @@ public partial class SettingsPaneHotkey
}
base.OnNavigatedTo(e);
}
private void PluginHotkeySettings_Loaded(object sender, RoutedEventArgs e)
{
var pluginHotkeyInfos = PluginManager.GetPluginHotkeyInfo();
foreach (var info in pluginHotkeyInfos)
{
var pluginPair = info.Key;
var hotkeyInfo = info.Value;
var metadata = pluginPair.Metadata;
// Skip this plugin if all hotkeys are invisible
var allHotkeyInvisible = hotkeyInfo.All(h => !h.Visible);
if (allHotkeyInvisible) continue;
var excard = new SettingsExpander()
{
Header = metadata.Name + " " + Localize.hotkeys(),
Margin = new Thickness(0, 4, 0, 0),
HeaderIcon = new Image() { Source = ImageLoader.LoadingImage },
Tag = metadata
};
var sortedHotkeyInfo = hotkeyInfo.OrderBy(h => h.Id).ToList();
foreach (var hotkey in sortedHotkeyInfo)
{
// Skip invisible hotkeys
if (!hotkey.Visible) continue;
var card = new SettingsCard()
{
Header = hotkey.Name,
Description = hotkey.Description,
HeaderIcon = new FontIcon() { Glyph = hotkey.Glyph.Glyph }
};
var hotkeySetting = metadata.PluginHotkeys.Find(h => h.Id == hotkey.Id)?.Hotkey ?? hotkey.DefaultHotkey;
if (hotkey.Editable)
{
var hotkeyControl = new HotkeyControl
{
Type = hotkey.HotkeyType == HotkeyType.Global ?
HotkeyControl.HotkeyType.GlobalPluginHotkey :
HotkeyControl.HotkeyType.WindowPluginHotkey,
DefaultHotkey = hotkey.DefaultHotkey,
ValidateKeyGesture = false,
Hotkey = hotkeySetting,
ChangeHotkey = new RelayCommand<HotkeyModel>((h) => ChangePluginHotkey(metadata, hotkey, h))
};
card.Content = hotkeyControl;
}
else
{
var hotkeyDisplay = new HotkeyDisplay
{
Keys = hotkeySetting
};
card.Content = hotkeyDisplay;
}
excard.Items.Add(card);
}
PluginHotkeySettings.Children.Add(excard);
}
// Load plugin icons into SettingsExpander asynchronously
_ = LoadPluginIconsAsync();
}
private static void ChangePluginHotkey(PluginMetadata metadata, BasePluginHotkey pluginHotkey, HotkeyModel newHotkey)
{
if (pluginHotkey is GlobalPluginHotkey globalPluginHotkey)
{
PluginManager.ChangePluginHotkey(metadata, globalPluginHotkey, newHotkey);
}
else if (pluginHotkey is SearchWindowPluginHotkey windowPluginHotkey)
{
PluginManager.ChangePluginHotkey(metadata, windowPluginHotkey, newHotkey);
}
}
private async Task LoadPluginIconsAsync()
{
// Snapshot list to avoid collection modification issues
var expanders = PluginHotkeySettings.Children
.OfType<SettingsExpander>()
.Where(e => e.Tag is PluginMetadata m && !string.IsNullOrEmpty(m.IcoPath))
.ToList();
// Fire all loads concurrently
var tasks = expanders.Select(async expander =>
{
if (expander.Tag is not PluginMetadata metadata) return;
try
{
var iconSource = await App.API.LoadImageAsync(metadata.IcoPath);
if (iconSource == null) return;
// Marshal back to UI thread if needed
if (!Dispatcher.CheckAccess())
{
await Dispatcher.InvokeAsync(() => ApplyIcon(expander, iconSource));
}
else
{
ApplyIcon(expander, iconSource);
}
}
catch
{
// Swallow exceptions to avoid impacting UI; optionally log if logging infra exists
}
});
await Task.WhenAll(tasks);
}
private static void ApplyIcon(SettingsExpander expander, ImageSource iconSource)
{
if (expander.HeaderIcon is Image img)
{
img.Source = iconSource;
}
else
{
expander.HeaderIcon = new Image { Source = iconSource };
}
}
}

View file

@ -585,6 +585,13 @@ namespace Flow.Launcher.ViewModel
#region BasicCommands
[RelayCommand]
private void CheckAndToggleFlowLauncher()
{
if (!ShouldIgnoreHotkeys())
ToggleFlowLauncher();
}
[RelayCommand]
private void OpenSetting()
{
@ -1874,6 +1881,11 @@ namespace Flow.Launcher.ViewModel
return selected;
}
internal ResultsViewModel GetSelectedResults()
{
return SelectedResults;
}
#endregion
#region Hotkey

View file

@ -10,6 +10,8 @@ using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using Flow.Launcher.Plugin.Explorer.Views;
using System.Windows.Controls;
namespace Flow.Launcher.Plugin.Explorer
{
@ -177,6 +179,35 @@ namespace Flow.Launcher.Plugin.Explorer
IcoPath = icoPath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uf12b")
});
contextMenus.Add(new Result
{
Title = Localize.plugin_explorer_rename_a_file(),
SubTitle = Localize.plugin_explorer_rename_subtitle(),
Action = _ =>
{
RenameFile window;
switch (record.Type)
{
case ResultType.Folder:
window = new RenameFile(new DirectoryInfo(record.FullPath));
break;
case ResultType.File:
window = new RenameFile(new FileInfo(record.FullPath));
break;
default:
Context.API.ShowMsgError(Localize.plugin_explorer_cannot_rename());
return false;
}
window.ShowDialog();
return false;
},
// placeholder until real image is found
IcoPath = Constants.RenameImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8ac")
});
if (record.Type is ResultType.File or ResultType.Folder)
contextMenus.Add(new Result

View file

@ -0,0 +1,137 @@
using System;
using System.IO;
using System.Runtime.Serialization;
namespace Flow.Launcher.Plugin.Explorer.Helper;
public static class RenameThing
{
private static void Rename(this FileSystemInfo info, string newName)
{
if (info is FileInfo file)
{
if (!SharedCommands.FilesFolders.IsValidFileName(newName))
{
throw new InvalidNameException();
}
DirectoryInfo directory;
var rootPath = Path.GetPathRoot(file.FullName);
if (string.IsNullOrEmpty(rootPath)) return;
directory = file.Directory ?? new DirectoryInfo(rootPath);
string newPath = Path.Join(directory.FullName, newName);
if (info.FullName == newPath)
{
throw new NotANewNameException("New name was the same as the old name");
}
if (File.Exists(newPath)) throw new ElementAlreadyExistsException();
File.Move(info.FullName, newPath);
return;
}
else if (info is DirectoryInfo directory)
{
if (!SharedCommands.FilesFolders.IsValidDirectoryName(newName))
{
throw new InvalidNameException();
}
DirectoryInfo parent;
var rootPath = Path.GetPathRoot(directory.FullName);
if (string.IsNullOrEmpty(rootPath)) return;
parent = directory.Parent ?? new DirectoryInfo(rootPath);
string newPath = Path.Join(parent.FullName, newName);
if (info.FullName == newPath)
{
throw new NotANewNameException("New name was the same as the old name");
}
if (Directory.Exists(newPath)) throw new ElementAlreadyExistsException();
Directory.Move(info.FullName, newPath);
}
else
{
throw new ArgumentException($"{nameof(info)} must be either, {nameof(FileInfo)} or {nameof(DirectoryInfo)}");
}
}
/// <summary>
/// Renames a file system element (directory or file)
/// </summary>
/// <param name="NewFileName">The requested new name</param>
/// <param name="oldInfo"> The <see cref="FileInfo"/> or <see cref="DirectoryInfo"/> representing the old file</param>
/// <param name="api">An instance of <see cref="IPublicAPI"/>so this can create msgboxes</param>
public static void Rename(string NewFileName, FileSystemInfo oldInfo)
{
// if it's just whitespace and nothing else
if (string.IsNullOrWhiteSpace(NewFileName))
{
Main.Context.API.ShowMsgError(Localize.plugin_explorer_field_may_not_be_empty());
return;
}
try
{
oldInfo.Rename(NewFileName);
}
catch (Exception exception)
{
switch (exception)
{
case FileNotFoundException:
Main.Context.API.ShowMsgError(Localize.plugin_explorer_item_not_found(oldInfo.FullName));
return;
case NotANewNameException:
Main.Context.API.ShowMsgError(Localize.plugin_explorer_not_a_new_name(NewFileName));
return;
case InvalidNameException:
Main.Context.API.ShowMsgError(Localize.plugin_explorer_invalid_name(NewFileName));
return;
case ElementAlreadyExistsException:
Main.Context.API.ShowMsgError(Localize.plugin_explorer_element_already_exists(NewFileName));
return;
default:
string msg = exception.Message;
if (!string.IsNullOrEmpty(msg))
{
Main.Context.API.ShowMsgError(Localize.plugin_explorer_exception(exception.Message));
return;
}
else
{
Main.Context.API.ShowMsgError(Localize.plugin_explorer_no_reason_given_exception());
}
return;
}
}
Main.Context.API.ShowMsg(Localize.plugin_explorer_successful_rename(NewFileName));
}
}
internal class NotANewNameException : IOException
{
public NotANewNameException() { }
public NotANewNameException(string message) : base(message) { }
public NotANewNameException(string message, Exception inner) : base(message, inner) { }
protected NotANewNameException(
SerializationInfo info,
StreamingContext context) : base(info, context) { }
}
internal class ElementAlreadyExistsException : IOException {
public ElementAlreadyExistsException() { }
public ElementAlreadyExistsException(string message) : base(message) { }
public ElementAlreadyExistsException(string message, Exception inner) : base(message, inner) { }
protected ElementAlreadyExistsException(
SerializationInfo info,
StreamingContext context) : base(info, context) { }
}
internal class InvalidNameException : Exception
{
public InvalidNameException() { }
public InvalidNameException(string message) : base(message) { }
public InvalidNameException(string message, Exception inner) : base(message, inner) { }
protected InvalidNameException(
SerializationInfo info,
StreamingContext context) : base(info, context) { }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

View file

@ -140,6 +140,7 @@
<system:String x:Key="plugin_explorer_fail_to_open">Fail to open {0}</system:String>
<system:String x:Key="plugin_explorer_fail_to_set_text">Fail to set text in clipboard</system:String>
<system:String x:Key="plugin_explorer_fail_to_set_files">Fail to set files/folders in clipboard</system:String>
<system:String x:Key="plugin_explorer_run_as_administrator">Run As Administrator</system:String>
<!-- Special Results -->
<system:String x:Key="plugin_explorer_diskfreespace">{0} free of {1}</system:String>
@ -212,4 +213,20 @@
<system:String x:Key="MonthsAgo">{0} months ago</system:String>
<system:String x:Key="OneYearAgo">1 year ago</system:String>
<system:String x:Key="YearsAgo">{0} years ago</system:String>
<!-- Rename File Dialog -->
<system:String x:Key="plugin_explorer_new_file_name">New name</system:String>
<system:String x:Key="plugin_explorer_rename_file_done">Rename</system:String>
<system:String x:Key="plugin_explorer_rename_a_file">Rename</system:String>
<system:String x:Key="plugin_explorer_not_a_new_name">The given name: {0} was not new.</system:String>
<system:String x:Key="plugin_explorer_field_may_not_be_empty">New file name should not be empty.</system:String>
<system:String x:Key="plugin_explorer_invalid_name">{0} is an invalid name.</system:String>
<system:String x:Key="plugin_explorer_item_not_found">The specified item: {0} was not found</system:String>
<system:String x:Key="plugin_explorer_rename_subtitle">Open a dialog to rename file or folder</system:String>
<system:String x:Key="plugin_explorer_cannot_rename">This cannot be renamed.</system:String>
<system:String x:Key="plugin_explorer_successful_rename">Successfully renamed it to: {0}</system:String>
<system:String x:Key="plugin_explorer_element_already_exists">There is already a file with the name: {0} in this location</system:String>
<system:String x:Key="plugin_explorer_failed_to_open_rename_dialog">Failed to open rename dialog.</system:String>
<system:String x:Key="plugin_explorer_exception">An error occurred: {0}.</system:String>
<system:String x:Key="plugin_explorer_no_reason_given_exception">An error occurred and no reason was given.</system:String>
</ResourceDictionary>

View file

@ -1,26 +1,28 @@
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using Flow.Launcher.Plugin.Explorer.Views;
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using Flow.Launcher.Plugin.Explorer.Exceptions;
using System.Linq;
using System.Globalization;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using Flow.Launcher.Plugin.Explorer.Views;
namespace Flow.Launcher.Plugin.Explorer
{
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncDialogJump
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncDialogJump, IPluginHotkey
{
internal static PluginInitContext Context { get; set; }
internal static Settings Settings { get; set; }
private static readonly string ClassName = nameof(Main);
private SettingsViewModel viewModel;
private ContextMenu contextMenu;
@ -48,6 +50,7 @@ namespace Flow.Launcher.Plugin.Explorer
EverythingApiDllImport.Load(Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "EverythingSDK",
Environment.Is64BitProcess ? "x64" : "x86"));
return Task.CompletedTask;
}
@ -128,5 +131,148 @@ namespace Flow.Launcher.Plugin.Explorer
return _emptyDialogJumpResultList;
}
}
public List<BasePluginHotkey> GetPluginHotkeys()
{
return new List<BasePluginHotkey>
{
new SearchWindowPluginHotkey()
{
Id = 0,
Name = Localize.plugin_explorer_opencontainingfolder(),
Description = Localize.plugin_explorer_opencontainingfolder_subtitle(),
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue838"),
DefaultHotkey = "Ctrl+Enter",
Editable = false,
Visible = true,
Action = (r) =>
{
if (r.ContextData is SearchResult record)
{
if (record.Type is ResultType.File)
{
ResultManager.OpenFolder(record.FullPath, record.FullPath);
}
else
{
try
{
Context.API.OpenDirectory(Path.GetDirectoryName(record.FullPath), record.FullPath);
}
catch (Exception e)
{
var message = $"Fail to open file at {record.FullPath}";
Context.API.LogException(ClassName, message, e);
Context.API.ShowMsgBox(e.Message, Localize.plugin_explorer_opendir_error());
return false;
}
return true;
}
}
return false;
}
},
new SearchWindowPluginHotkey()
{
Id = 1,
Name = Localize.plugin_explorer_show_contextmenu_title(),
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue700"),
DefaultHotkey = "Alt+Enter",
Editable = false,
Visible = true,
Action = (r) =>
{
if (r.ContextData is SearchResult record && record.Type is not ResultType.Volume)
{
try
{
ResultManager.ShowNativeContextMenu(record.FullPath, record.Type);
}
catch (Exception e)
{
var message = $"Fail to show context menu for {record.FullPath}";
Context.API.LogException(ClassName, message, e);
}
}
return false;
}
},
new SearchWindowPluginHotkey()
{
Id = 2,
Name = Localize.plugin_explorer_run_as_administrator(),
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE7EF"),
DefaultHotkey = "Ctrl+Shift+Enter",
Editable = false,
Visible = true,
Action = (r) =>
{
if (r.ContextData is SearchResult record)
{
if (record.Type is ResultType.File)
{
var filePath = record.FullPath;
ResultManager.OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, true);
}
else
{
try
{
ResultManager.OpenFolder(record.FullPath);
return true;
}
catch (Exception ex)
{
var message = $"Fail to open file at {record.FullPath}";
Context.API.LogException(ClassName, message, ex);
Context.API.ShowMsgBox(ex.Message, Localize.plugin_explorer_opendir_error());
return false;
}
}
return true;
}
return false;
}
},
new SearchWindowPluginHotkey()
{
Id = 3,
Name = Localize.plugin_explorer_rename_a_file(),
Description = Localize.plugin_explorer_rename_subtitle(),
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8ac"),
DefaultHotkey = "F2",
Editable = true,
Visible = true,
Action = (r) =>
{
if (r.ContextData is SearchResult record)
{
RenameFile window;
switch (record.Type)
{
case ResultType.Folder:
window = new RenameFile(new DirectoryInfo(record.FullPath));
break;
case ResultType.File:
window = new RenameFile(new FileInfo(record.FullPath));
break;
default:
Context.API.ShowMsgError(Localize.plugin_explorer_cannot_rename());
return false;
}
window.ShowDialog();
return false;
}
return false;
}
}
};
}
}
}

View file

@ -17,6 +17,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal const string QuickAccessImagePath = "Images\\quickaccess.png";
internal const string RemoveQuickAccessImagePath = "Images\\removequickaccess.png";
internal const string ShowContextMenuImagePath = "Images\\context_menu.png";
internal const string RenameImagePath = "Images\\rename.png";
internal const string EverythingErrorImagePath = "Images\\everything_error.png";
internal const string IndexSearchWarningImagePath = "Images\\index_error.png";
internal const string WindowsIndexErrorImagePath = "Images\\index_error2.png";

View file

@ -1,9 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Input;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Views;
using Flow.Launcher.Plugin.SharedCommands;
@ -109,40 +109,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search
PreviewPanel = new Lazy<UserControl>(() => new PreviewPanel(Settings, path, ResultType.Folder)),
Action = c =>
{
if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt)
{
ShowNativeContextMenu(path, ResultType.Folder);
return false;
}
// open folder
if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift))
{
try
{
OpenFolder(path);
return true;
}
catch (Exception ex)
{
Context.API.ShowMsgBox(ex.Message, Localize.plugin_explorer_opendir_error());
return false;
}
}
// Open containing folder
if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control)
{
try
{
Context.API.OpenDirectory(Path.GetDirectoryName(path), path);
return true;
}
catch (Exception ex)
{
Context.API.ShowMsgBox(ex.Message, Localize.plugin_explorer_opendir_error());
return false;
}
}
// If path search is disabled just open it in file manager
if (Settings.DefaultOpenFolderInFileManager || (!Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled))
{
@ -168,7 +134,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Score = score,
TitleToolTip = Localize.plugin_explorer_plugin_ToolTipOpenDirectory(),
SubTitleToolTip = Settings.DisplayMoreInformationInToolTip ? GetFolderMoreInfoTooltip(path) : path,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed }
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed },
HotkeyIds = new List<int>
{
0, 1, 2, 3
},
};
}
@ -270,15 +240,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search
CopyText = folderPath,
Action = c =>
{
if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt)
{
ShowNativeContextMenu(folderPath, ResultType.Folder);
return false;
}
OpenFolder(folderPath);
return true;
},
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed }
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed },
HotkeyIds = new List<int>
{
1
},
};
}
@ -308,25 +277,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search
PreviewPanel = new Lazy<UserControl>(() => new PreviewPanel(Settings, filePath, ResultType.File)),
Action = c =>
{
if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt)
{
ShowNativeContextMenu(filePath, ResultType.File);
return false;
}
try
{
if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift))
{
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty, true);
}
else if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control)
{
OpenFolder(filePath, filePath);
}
else
{
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty);
}
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty);
}
catch (Exception ex)
{
@ -337,7 +290,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search
},
TitleToolTip = Localize.plugin_explorer_plugin_ToolTipOpenContainingFolder(),
SubTitleToolTip = Settings.DisplayMoreInformationInToolTip ? GetFileMoreInfoTooltip(filePath) : filePath,
ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed }
ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed },
HotkeyIds = new List<int>
{
0, 1, 2, 3
},
};
return result;
}
@ -349,13 +306,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return MediaExtensions.Contains(extension.ToLowerInvariant());
}
private static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false)
public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false)
{
IncrementEverythingRunCounterIfNeeded(filePath);
FilesFolders.OpenFile(filePath, workingDir, asAdmin, (string str) => Context.API.ShowMsgBox(str));
}
private static void OpenFolder(string folderPath, string fileNameOrFilePath = null)
public static void OpenFolder(string folderPath, string fileNameOrFilePath = null)
{
IncrementEverythingRunCounterIfNeeded(folderPath);
Context.API.OpenDirectory(folderPath, fileNameOrFilePath);

View file

@ -0,0 +1,114 @@
<Window
x:Class="Flow.Launcher.Plugin.Explorer.Views.RenameFile"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title=""
Height="180"
MaxWidth="600"
Background="{DynamicResource PopuBGColor}"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize"
SizeToContent="Width"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="60" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="4"
Click="BtnCancel"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
</Grid>
</StackPanel>
<StackPanel Margin="26 0 26 0">
<StackPanel Margin="0 0 0 0">
<TextBlock
Margin="0 0 0 0"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource plugin_explorer_rename_a_file}"
TextAlignment="Left" />
</StackPanel>
<StackPanel Margin="0 10 0 0" Orientation="Horizontal">
<TextBlock
MinWidth="150"
Margin="0 10 5 10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource plugin_explorer_new_file_name}" />
<TextBox
Name="RenameTb"
Width="300"
HorizontalAlignment="Left"
VerticalAlignment="Center"
GotFocus="SelectAll_OnTextBoxGotFocus"
PreviewKeyDown="RenameTb_OnKeyDown"
Text="{Binding NewFileName}" />
</StackPanel>
</StackPanel>
</StackPanel>
<Border
Grid.Row="1"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0 1 0 0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
x:Name="btnCancel"
Width="145"
Height="34"
Margin="5 0 5 0"
Click="BtnCancel"
Content="{DynamicResource cancel}" />
<Button
Name="btnDone"
Width="145"
Height="34"
Margin="5 0 5 0"
Click="OnDoneButtonClick"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource plugin_explorer_rename_file_done}" />
</Button>
</StackPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,97 @@
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Flow.Launcher.Plugin.Explorer.Helper;
namespace Flow.Launcher.Plugin.Explorer.Views
{
[INotifyPropertyChanged]
public partial class RenameFile : Window
{
public string NewFileName
{
get => _newFileName;
set
{
_ = SetProperty(ref _newFileName, value);
}
}
private string _newFileName;
private readonly string _oldFilePath;
private readonly FileSystemInfo _info;
public RenameFile(FileSystemInfo info)
{
_info = info;
_oldFilePath = _info.FullName;
NewFileName = _info.Name;
InitializeComponent();
ShowInTaskbar = false;
RenameTb.Focus();
KeyDown += (s, e) =>
{
if (e.Key == Key.Escape)
{
Close();
}
};
}
/// <summary>
/// https://stackoverflow.com/a/59560352/24045055
/// </summary>
private async void SelectAll_OnTextBoxGotFocus(object sender, RoutedEventArgs e)
{
if (sender is not TextBox textBox) return;
if (_info is DirectoryInfo)
{
await textBox.Dispatcher.InvokeAsync(textBox.SelectAll, DispatcherPriority.Background);
return;
}
// select everything but the extension
if (_info is FileInfo info)
{
string properName = Path.GetFileNameWithoutExtension(info.Name);
int start = textBox.Text.LastIndexOf(properName, StringComparison.OrdinalIgnoreCase);
if (start < 0)
{
await textBox.Dispatcher.InvokeAsync(textBox.SelectAll, DispatcherPriority.Background);
return;
}
await textBox.Dispatcher.InvokeAsync(() => textBox.Select(start, properName.Length), DispatcherPriority.Background);
}
}
private void OnDoneButtonClick(object sender, RoutedEventArgs e)
{
RenameThing.Rename(NewFileName, _info);
// Close the dialog no matter if it worked or not because error messages are popped up in RenameThing
Close();
}
private void BtnCancel(object sender, RoutedEventArgs e)
{
Close();
}
private void RenameTb_OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
btnDone.Focus();
OnDoneButtonClick(sender, e);
e.Handled = true;
}
}
}
}

View file

@ -1,14 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using Flow.Launcher.Plugin.PluginsManager.ViewModels;
using Flow.Launcher.Plugin.PluginsManager.Views;
namespace Flow.Launcher.Plugin.PluginsManager
{
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IPluginHotkey
{
internal static PluginInitContext Context { get; set; }
@ -69,5 +69,36 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description");
}
public List<BasePluginHotkey> GetPluginHotkeys()
{
return new List<BasePluginHotkey>
{
new SearchWindowPluginHotkey
{
Id = 0,
Name = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_openwebsite_title"),
Description = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_openwebsite_subtitle"),
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uEB41"),
DefaultHotkey = "Ctrl+Enter",
Editable = false,
Visible = true,
Action = (r) =>
{
if (r.ContextData is UserPlugin plugin)
{
if (!string.IsNullOrWhiteSpace(plugin.Website))
{
Context.API.OpenUrl(plugin.Website);
return true;
}
}
return false;
}
}
};
}
}
}

View file

@ -421,12 +421,15 @@ namespace Flow.Launcher.Plugin.PluginsManager
return true;
},
ContextData =
new UserPlugin
{
Website = x.PluginNewUserPlugin.Website,
UrlSourceCode = x.PluginNewUserPlugin.UrlSourceCode
}
ContextData = new UserPlugin
{
Website = x.PluginNewUserPlugin.Website,
UrlSourceCode = x.PluginNewUserPlugin.UrlSourceCode
},
HotkeyIds = new List<int>
{
0
},
});
// Update all result
@ -574,12 +577,6 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = icoPath,
Action = e =>
{
if (e.SpecialKeyState.CtrlPressed)
{
SearchWeb.OpenInBrowserTab(plugin.UrlDownload);
return ShouldHideWindow;
}
if (Settings.WarnFromUnknownSource)
{
if (!InstallSourceKnown(plugin.UrlDownload)
@ -697,17 +694,15 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = x.IcoPath,
Action = e =>
{
if (e.SpecialKeyState.CtrlPressed)
{
SearchWeb.OpenInBrowserTab(x.Website);
return ShouldHideWindow;
}
Context.API.HideMainWindow();
_ = InstallOrUpdateAsync(x); // No need to wait
return ShouldHideWindow;
},
ContextData = x
ContextData = x,
HotkeyIds = new List<int>
{
0
},
});
return Search(results, search);
@ -816,7 +811,15 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
return false;
}
},
ContextData = new UserPlugin
{
Website = x.Metadata.Website
},
HotkeyIds = new List<int>
{
0
},
});
return Search(results, search);

View file

@ -15,7 +15,7 @@ using Path = System.IO.Path;
namespace Flow.Launcher.Plugin.Program
{
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable, IPluginHotkey
{
private static readonly string ClassName = nameof(Main);
@ -560,5 +560,59 @@ namespace Flow.Launcher.Plugin.Program
{
Win32.Dispose();
}
public List<BasePluginHotkey> GetPluginHotkeys()
{
return new List<BasePluginHotkey>
{
new SearchWindowPluginHotkey()
{
Id = 0,
Name = Context.API.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue838"),
DefaultHotkey = "Ctrl+Enter",
Editable = false,
Visible = true,
Action = (r) =>
{
if (r?.ContextData is UWPApp uwp)
{
Context.API.OpenDirectory(uwp.Location);
return true;
}
else if (r?.ContextData is Win32 win32)
{
Context.API.OpenDirectory(win32.ParentDirectory, win32.FullPath);
return true;
}
return false;
}
},
// TODO: Do it after administrator mode PR
/*new SearchWindowPluginHotkey()
{
Id = 1,
Name = Context.API.GetTranslation("flowlauncher_plugin_program_run_as_administrator"),
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE7EF"),
DefaultHotkey = "Ctrl+Shift+Enter",
Editable = false,
Visible = true,
Action = (r) =>
{
if (r.ContextData is UWPApp uwp)
{
return true;
}
else if (r.ContextData is Win32 win32)
{
return true;
}
return false;
}
},*/
};
}
}
}

View file

@ -443,14 +443,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
ContextData = this,
Action = e =>
{
// Ctrl + Enter to open containing folder
bool openFolder = e.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control;
if (openFolder)
{
Main.Context.API.OpenDirectory(Location);
return true;
}
// Ctrl + Shift + Enter to run elevated
bool elevated = e.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift);
@ -466,7 +458,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
return true;
}
},
HotkeyIds = new List<int>
{
0
},
};
return result;

View file

@ -185,14 +185,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
TitleToolTip = $"{title}\n{ExecutablePath}",
Action = c =>
{
// Ctrl + Enter to open containing folder
bool openFolder = c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control;
if (openFolder)
{
Main.Context.API.OpenDirectory(ParentDirectory, FullPath);
return true;
}
// Ctrl + Shift + Enter to run as admin
bool runAsAdmin = c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift);
@ -207,7 +199,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
_ = Task.Run(() => Main.StartProcess(Process.Start, info));
return true;
}
},
HotkeyIds = new List<int>
{
0
},
};
return result;

View file

@ -286,16 +286,12 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea
| ------------------------------------------------------------------------- | ----------------------------------------------- |
| <kbd>Alt</kbd>+<kbd>Space</kbd> | Open search window (default and configurable) |
| <kbd>Enter</kbd> | Execute |
| <kbd>Ctrl</kbd>+<kbd>Enter</kbd> | Open containing folder |
| <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Enter</kbd> | Run as admin |
| <kbd></kbd>/<kbd></kbd>, <kbd>Shift</kbd>+<kbd>Tab</kbd>/<kbd>Tab</kbd> | Previous / Next result |
| <kbd></kbd>/<kbd></kbd> | Back to result / Open Context Menu |
| <kbd>Ctrl</kbd>+<kbd>O</kbd> , <kbd>Shift</kbd>+<kbd>Enter</kbd> | Open Context Menu |
| <kbd>Ctrl</kbd>+<kbd>Tab</kbd> | Autocomplete |
| <kbd>F1</kbd> | Toggle Preview Panel (default and configurable) |
| <kbd>Esc</kbd> | Back to results / hide search window |
| <kbd>Ctrl</kbd>+<kbd>C</kbd> | Copy folder / file |
| <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>C</kbd> | Copy folder / file path |
| <kbd>Ctrl</kbd>+<kbd>I</kbd> | Open Flow's settings |
| <kbd>Ctrl</kbd>+<kbd>R</kbd> | Run the current query again (refresh results) |
| <kbd>F5</kbd> | Reload all plugin data |