Dialog Jump - Quickly navigate the Open/Save As dialog window (#1018)

This commit is contained in:
Kevin Zhang 2025-07-20 04:11:09 -07:00 committed by GitHub
parent ff2d5e89f9
commit 4f269d3fa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2877 additions and 103 deletions

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@ -9,6 +9,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -40,6 +41,9 @@ namespace Flow.Launcher.Core.Plugin
private static IEnumerable<PluginPair> _resultUpdatePlugin;
private static IEnumerable<PluginPair> _translationPlugins;
private static readonly List<DialogJumpExplorerPair> _dialogJumpExplorerPlugins = new();
private static readonly List<DialogJumpDialogPair> _dialogJumpDialogPlugins = new();
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
@ -186,6 +190,24 @@ namespace Flow.Launcher.Core.Plugin
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
_resultUpdatePlugin = GetPluginsForInterface<IResultUpdated>();
_translationPlugins = GetPluginsForInterface<IPluginI18n>();
// Initialize Dialog Jump plugin pairs
foreach (var pair in GetPluginsForInterface<IDialogJumpExplorer>())
{
_dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair
{
Plugin = (IDialogJumpExplorer)pair.Plugin,
Metadata = pair.Metadata
});
}
foreach (var pair in GetPluginsForInterface<IDialogJumpDialog>())
{
_dialogJumpDialogPlugins.Add(new DialogJumpDialogPair
{
Plugin = (IDialogJumpDialog)pair.Plugin,
Metadata = pair.Metadata
});
}
}
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
@ -288,20 +310,24 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static ICollection<PluginPair> ValidPluginsForQuery(Query query)
public static ICollection<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump)
{
if (query is null)
return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
{
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
if (dialogJump)
return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList();
else
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
if (API.PluginModified(plugin.Metadata.ID))
{
if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
return Array.Empty<PluginPair>();
if (API.PluginModified(plugin.Metadata.ID))
return Array.Empty<PluginPair>();
}
return new List<PluginPair>
{
@ -388,6 +414,36 @@ namespace Flow.Launcher.Core.Plugin
return results;
}
public static async Task<List<DialogJumpResult>> QueryDialogJumpForPluginAsync(PluginPair pair, Query query, CancellationToken token)
{
var results = new List<DialogJumpResult>();
var metadata = pair.Metadata;
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
if (results == null)
return null;
UpdatePluginMetadata(results, metadata, query);
token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException)
{
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null;
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e);
return null;
}
return results;
}
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
{
foreach (var r in results)
@ -463,6 +519,16 @@ namespace Flow.Launcher.Core.Plugin
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
}
public static IList<DialogJumpExplorerPair> GetDialogJumpExplorers()
{
return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static IList<DialogJumpDialogPair> GetDialogJumpDialogs()
{
return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static bool ActionKeywordRegistered(string actionKeyword)
{
// this method is only checking for action keywords (defined as not '*') registration

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.DialogJump;
public class DialogJumpExplorerPair
{
public IDialogJumpExplorer Plugin { get; init; }
public PluginMetadata Metadata { get; init; }
public override string ToString()
{
return Metadata.Name;
}
public override bool Equals(object obj)
{
if (obj is DialogJumpExplorerPair r)
{
return string.Equals(r.Metadata.ID, Metadata.ID);
}
else
{
return false;
}
}
public override int GetHashCode()
{
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
return hashcode;
}
}
public class DialogJumpDialogPair
{
public IDialogJumpDialog Plugin { get; init; }
public PluginMetadata Metadata { get; init; }
public override string ToString()
{
return Metadata.Name;
}
public override bool Equals(object obj)
{
if (obj is DialogJumpDialogPair r)
{
return string.Equals(r.Metadata.ID, Metadata.ID);
}
else
{
return false;
}
}
public override int GetHashCode()
{
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
return hashcode;
}
}

View file

@ -0,0 +1,345 @@
using System;
using System.Threading;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using WindowsInput;
using WindowsInput.Native;
namespace Flow.Launcher.Infrastructure.DialogJump.Models
{
/// <summary>
/// Class for handling Windows File Dialog instances in DialogJump.
/// </summary>
public class WindowsDialog : IDialogJumpDialog
{
private const string WindowsDialogClassName = "#32770";
public IDialogJumpDialogWindow CheckDialogWindow(IntPtr hwnd)
{
// Is it a Win32 dialog box?
if (GetClassName(new(hwnd)) == WindowsDialogClassName)
{
// Is it a windows file dialog?
var dialogType = GetFileDialogType(new(hwnd));
if (dialogType != DialogType.Others)
{
return new WindowsDialogWindow(hwnd, dialogType);
}
}
return null;
}
public void Dispose()
{
}
#region Help Methods
private static unsafe string GetClassName(HWND handle)
{
fixed (char* buf = new char[256])
{
return PInvoke.GetClassName(handle, buf, 256) switch
{
0 => string.Empty,
_ => new string(buf),
};
}
}
private static DialogType GetFileDialogType(HWND handle)
{
// Is it a Windows Open file dialog?
var fileEditor = PInvoke.GetDlgItem(handle, 0x047C);
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open;
// Is it a Windows Save or Save As file dialog?
fileEditor = PInvoke.GetDlgItem(handle, 0x0000);
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs;
return DialogType.Others;
}
#endregion
}
public class WindowsDialogWindow : IDialogJumpDialogWindow
{
public IntPtr Handle { get; private set; } = IntPtr.Zero;
// After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore
// So we need to cache the current tab and use the original handle
private IDialogJumpDialogWindowTab _currentTab { get; set; } = null;
private readonly DialogType _dialogType;
internal WindowsDialogWindow(IntPtr handle, DialogType dialogType)
{
Handle = handle;
_dialogType = dialogType;
}
public IDialogJumpDialogWindowTab GetCurrentTab()
{
return _currentTab ??= new WindowsDialogTab(Handle, _dialogType);
}
public void Dispose()
{
}
}
public class WindowsDialogTab : IDialogJumpDialogWindowTab
{
#region Public Properties
public IntPtr Handle { get; private set; } = IntPtr.Zero;
#endregion
#region Private Fields
private static readonly string ClassName = nameof(WindowsDialogTab);
private static readonly InputSimulator _inputSimulator = new();
private readonly DialogType _dialogType;
private bool _legacy { get; set; } = false;
private HWND _pathControl { get; set; } = HWND.Null;
private HWND _pathEditor { get; set; } = HWND.Null;
private HWND _fileEditor { get; set; } = HWND.Null;
private HWND _openButton { get; set; } = HWND.Null;
#endregion
#region Constructor
internal WindowsDialogTab(IntPtr handle, DialogType dialogType)
{
Handle = handle;
_dialogType = dialogType;
Log.Debug(ClassName, $"File dialog type: {dialogType}");
}
#endregion
#region Public Methods
public string GetCurrentFolder()
{
if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty;
return GetWindowText(_pathEditor);
}
public string GetCurrentFile()
{
if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty;
return GetWindowText(_fileEditor);
}
public bool JumpFolder(string path, bool auto)
{
if (auto)
{
// Use legacy jump folder method for auto Dialog Jump because file editor is default value.
// After setting path using file editor, we do not need to revert its value.
return JumpFolderWithFileEditor(path, false);
}
// Alt-D or Ctrl-L to focus on the path input box
// "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus
_inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D);
// _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L);
if (_pathControl.IsNull && !GetPathControlEditor())
{
// https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1
// The dialog is a legacy one, so we can only edit file editor directly.
Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
var timeOut = !SpinWait.SpinUntil(() =>
{
var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0;
}, 1000);
if (timeOut)
{
// Path control is not visible, so we can only edit file editor directly.
Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
if (_pathEditor.IsNull)
{
// Path editor cannot be found, so we can only edit file editor directly.
Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
SetWindowText(_pathEditor, path);
_inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN);
return true;
}
public bool JumpFile(string path)
{
if (_fileEditor.IsNull && !GetFileEditor()) return false;
SetWindowText(_fileEditor, path);
return true;
}
public bool Open()
{
if (_openButton.IsNull && !GetOpenButton()) return false;
PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
return true;
}
public void Dispose()
{
}
#endregion
#region Helper Methods
#region Get Handles
private bool GetPathControlEditor()
{
// Get the handle of the path editor
// Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control
_pathControl = PInvoke.FindWindowEx(new(Handle), HWND.Null, "WorkerW", null); // 0x0000
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205
if (_pathControl == HWND.Null)
{
_pathEditor = HWND.Null;
_legacy = true;
Log.Info(ClassName, "Legacy dialog");
}
else
{
_pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox
_pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit
if (_pathEditor == HWND.Null)
{
_legacy = true;
Log.Error(ClassName, "Failed to find path editor handle");
}
}
return !_legacy;
}
private bool GetFileEditor()
{
if (_dialogType == DialogType.Open)
{
// Get the handle of the file name editor of Open file dialog
_fileEditor = PInvoke.GetDlgItem(new(Handle), 0x047C); // ComboBoxEx32
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit
}
else
{
// Get the handle of the file name editor of Save / SaveAs file dialog
_fileEditor = PInvoke.GetDlgItem(new(Handle), 0x0000); // DUIViewWndClassName
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit
}
if (_fileEditor == HWND.Null)
{
Log.Error(ClassName, "Failed to find file name editor handle");
return false;
}
return true;
}
private bool GetOpenButton()
{
// Get the handle of the open button
_openButton = PInvoke.GetDlgItem(new(Handle), 0x0001); // Open/Save/SaveAs Button
if (_openButton == HWND.Null)
{
Log.Error(ClassName, "Failed to find open button handle");
return false;
}
return true;
}
#endregion
#region Windows Text
private static unsafe string GetWindowText(HWND handle)
{
int length;
Span<char> buffer = stackalloc char[1000];
fixed (char* pBuffer = buffer)
{
// If the control has no title bar or text, or if the control handle is invalid, the return value is zero.
length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer);
}
return buffer[..length].ToString();
}
private static unsafe nint SetWindowText(HWND handle, string text)
{
fixed (char* textPtr = text + '\0')
{
return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value;
}
}
#endregion
#region Legacy Jump Folder
private bool JumpFolderWithFileEditor(string path, bool resetFocus)
{
// For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors.
if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false;
if (_fileEditor.IsNull && !GetFileEditor()) return false;
SetWindowText(_fileEditor, path);
if (_openButton.IsNull && !GetOpenButton()) return false;
PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
return true;
}
#endregion
#endregion
}
internal enum DialogType
{
Others,
Open,
SaveOrSaveAs
}
}

View file

@ -0,0 +1,260 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Flow.Launcher.Plugin;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
namespace Flow.Launcher.Infrastructure.DialogJump.Models
{
/// <summary>
/// Class for handling Windows Explorer instances in DialogJump.
/// </summary>
public class WindowsExplorer : IDialogJumpExplorer
{
public IDialogJumpExplorerWindow CheckExplorerWindow(IntPtr hwnd)
{
IDialogJumpExplorerWindow explorerWindow = null;
// Is it from Explorer?
var processName = Win32Helper.GetProcessNameFromHwnd(new(hwnd));
if (processName.Equals("explorer.exe", StringComparison.OrdinalIgnoreCase))
{
EnumerateShellWindows((shellWindow) =>
{
try
{
if (shellWindow is not IWebBrowser2 explorer) return true;
if (explorer.HWND != hwnd) return true;
explorerWindow = new WindowsExplorerWindow(hwnd);
return false;
}
catch
{
// Ignored
}
return true;
});
}
return explorerWindow;
}
internal static unsafe void EnumerateShellWindows(Func<object, bool> action)
{
// Create an instance of ShellWindows
var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass
var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows
var result = PInvoke.CoCreateInstance(
&clsidShellWindows,
null,
CLSCTX.CLSCTX_ALL,
&iidIShellWindows,
out var shellWindowsObj);
if (result.Failed) return;
var shellWindows = (IShellWindows)shellWindowsObj;
// Enumerate the shell windows
var count = shellWindows.Count;
for (var i = 0; i < count; i++)
{
if (!action(shellWindows.Item(i)))
{
return;
}
}
}
public void Dispose()
{
}
}
public class WindowsExplorerWindow : IDialogJumpExplorerWindow
{
public IntPtr Handle { get; }
private static Guid _shellBrowserGuid = typeof(IShellBrowser).GUID;
internal WindowsExplorerWindow(IntPtr handle)
{
Handle = handle;
}
public string GetExplorerPath()
{
if (Handle == IntPtr.Zero) return null;
var activeTabHandle = GetActiveTabHandle(new(Handle));
if (activeTabHandle.IsNull) return null;
var window = GetExplorerByTabHandle(activeTabHandle);
if (window == null) return null;
var path = GetLocation(window);
return path;
}
public void Dispose()
{
}
#region Helper Methods
// Inspired by: https://github.com/w4po/ExplorerTabUtility
private static HWND GetActiveTabHandle(HWND windowHandle)
{
// Active tab always at the top of the z-index, so it is the first child of the ShellTabWindowClass.
var activeTab = PInvoke.FindWindowEx(windowHandle, HWND.Null, "ShellTabWindowClass", null);
return activeTab;
}
private static IWebBrowser2 GetExplorerByTabHandle(HWND tabHandle)
{
if (tabHandle.IsNull) return null;
IWebBrowser2 window = null;
WindowsExplorer.EnumerateShellWindows((shellWindow) =>
{
try
{
return StartSTAThread(() =>
{
if (shellWindow is not IWebBrowser2 explorer) return true;
if (explorer is not IServiceProvider sp) return true;
sp.QueryService(ref _shellBrowserGuid, ref _shellBrowserGuid, out var shellBrowser);
if (shellBrowser == null) return true;
try
{
shellBrowser.GetWindow(out var hWnd); // Must execute in STA thread to get this hWnd
if (hWnd == tabHandle)
{
window = explorer;
return false;
}
}
catch
{
// Ignored
}
finally
{
Marshal.ReleaseComObject(shellBrowser);
}
return true;
}) ?? true;
}
catch
{
// Ignored
}
return true;
});
return window;
}
private static bool? StartSTAThread(Func<bool> action)
{
bool? result = null;
var thread = new Thread(() =>
{
result = action();
})
{
IsBackground = true
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return result;
}
private static string GetLocation(IWebBrowser2 window)
{
var path = window.LocationURL.ToString();
if (!string.IsNullOrWhiteSpace(path)) return NormalizeLocation(path);
// Recycle Bin, This PC, etc
if (window.Document is not IShellFolderViewDual folderView) return null;
// Attempt to get the path from the folder view
try
{
// CSWin32 Folder does not have Self, so we need to use dynamic type here
// Use dynamic to bypass static typing
dynamic folder = folderView.Folder;
// Access the Self property via dynamic binding
dynamic folderItem = folder.Self;
// Get path from the folder item
path = folderItem.Path;
}
catch
{
return null;
}
return NormalizeLocation(path);
}
private static string NormalizeLocation(string location)
{
if (location.IndexOf('%') > -1)
location = Environment.ExpandEnvironmentVariables(location);
if (location.StartsWith("::", StringComparison.Ordinal))
location = $"shell:{location}";
else if (location.StartsWith("{", StringComparison.Ordinal))
location = $"shell:::{location}";
location = location.Trim(' ', '/', '\\', '\n', '\'', '"');
return location.Replace('/', '\\');
}
#endregion
}
#region COM Interfaces
// Inspired by: https://github.com/w4po/ExplorerTabUtility
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("6d5140c1-7436-11ce-8034-00aa006009fa")]
[ComImport]
public interface IServiceProvider
{
[PreserveSig]
int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellBrowser ppvObject);
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214E2-0000-0000-C000-000000000046")]
[ComImport]
public interface IShellBrowser
{
[PreserveSig]
int GetWindow(out nint handle);
}
#endregion
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
@ -60,12 +60,14 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="MemoryPack" Version="1.21.4" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.12.19" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="NLog" Version="4.7.10" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
<PrivateAssets>all</PrivateAssets>

View file

@ -66,3 +66,27 @@ LOCALE_TRANSIENT_KEYBOARD4
SHParseDisplayName
SHOpenFolderAndSelectItems
CoTaskMemFree
SetWinEventHook
UnhookWinEvent
SendMessage
EVENT_SYSTEM_FOREGROUND
WINEVENT_OUTOFCONTEXT
WM_SETTEXT
IShellFolderViewDual2
CoCreateInstance
CLSCTX
IShellWindows
IWebBrowser2
EVENT_OBJECT_DESTROY
EVENT_OBJECT_LOCATIONCHANGE
EVENT_SYSTEM_MOVESIZESTART
EVENT_SYSTEM_MOVESIZEEND
GetDlgItem
PostMessage
BM_CLICK
WM_GETTEXT
OpenProcess
QueryFullProcessImageName
EVENT_OBJECT_HIDE
EVENT_SYSTEM_DIALOGEND

View file

@ -86,6 +86,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
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 _language = Constant.SystemLanguageCode;
public string Language
@ -323,6 +324,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
};
public bool EnableDialogJump { get; set; } = true;
public bool AutoDialogJump { get; set; } = false;
public bool ShowDialogJumpWindow { get; set; } = false;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpWindowPositions DialogJumpWindowPosition { get; set; } = DialogJumpWindowPositions.UnderDialog;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpResultBehaviours DialogJumpResultBehaviour { get; set; } = DialogJumpResultBehaviours.LeftClick;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpFileResultBehaviours DialogJumpFileResultBehaviour { get; set; } = DialogJumpFileResultBehaviours.FullPath;
[JsonConverter(typeof(JsonStringEnumConverter))]
public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO;
@ -546,6 +562,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
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)
@ -659,4 +677,23 @@ namespace Flow.Launcher.Infrastructure.UserSettings
DaNiu,
XiaoLang
}
public enum DialogJumpWindowPositions
{
UnderDialog,
FollowDefault
}
public enum DialogJumpResultBehaviours
{
LeftClick,
RightClick
}
public enum DialogJumpFileResultBehaviours
{
FullPath,
FullPathOpen,
Directory
}
}

View file

@ -14,9 +14,11 @@ using System.Windows.Markup;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.UserSettings;
using Microsoft.Win32;
using Microsoft.Win32.SafeHandles;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.System.Threading;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.Shell.Common;
using Windows.Win32.UI.WindowsAndMessaging;
@ -138,6 +140,11 @@ namespace Flow.Launcher.Infrastructure
return IsForegroundWindow(GetWindowHandle(window));
}
public static bool IsForegroundWindow(nint handle)
{
return IsForegroundWindow(new HWND(handle));
}
internal static bool IsForegroundWindow(HWND handle)
{
return handle.Equals(PInvoke.GetForegroundWindow());
@ -344,6 +351,16 @@ namespace Flow.Launcher.Infrastructure
return new(windowHelper.Handle);
}
internal static HWND GetMainWindowHandle()
{
// When application is exiting, the Application.Current will be null
if (Application.Current == null) return HWND.Null;
// Get the FL main window
var hwnd = GetWindowHandle(Application.Current.MainWindow, true);
return hwnd;
}
#endregion
#region STA Thread
@ -761,6 +778,65 @@ namespace Flow.Launcher.Infrastructure
#endregion
#region Window Rect
public static unsafe bool GetWindowRect(nint handle, out Rect outRect)
{
var rect = new RECT();
var result = PInvoke.GetWindowRect(new(handle), &rect);
if (!result)
{
outRect = new Rect();
return false;
}
// Convert RECT to Rect
outRect = new Rect(
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top
);
return true;
}
#endregion
#region Window Process
internal static unsafe string GetProcessNameFromHwnd(HWND hWnd)
{
return Path.GetFileName(GetProcessPathFromHwnd(hWnd));
}
internal static unsafe string GetProcessPathFromHwnd(HWND hWnd)
{
uint pid;
var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid);
if (threadId == 0) return string.Empty;
var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (process.Value != IntPtr.Zero)
{
using var safeHandle = new SafeProcessHandle(process.Value, true);
uint capacity = 2000;
Span<char> buffer = new char[capacity];
fixed (char* pBuffer = buffer)
{
if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, (PWSTR)pBuffer, ref capacity))
{
return string.Empty;
}
return buffer[..(int)capacity].ToString();
}
}
return string.Empty;
}
#endregion
#region Explorer
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems

View file

@ -0,0 +1,92 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Describes a result of a <see cref="Query"/> executed by a plugin in Dialog Jump window
/// </summary>
public class DialogJumpResult : Result
{
/// <summary>
/// This holds the path which can be provided by plugin to be navigated to the
/// file dialog when records in Dialog Jump window is right clicked on a result.
/// </summary>
public required string DialogJumpPath { get; init; }
/// <summary>
/// Clones the current Dialog Jump result
/// </summary>
public new DialogJumpResult Clone()
{
return new DialogJumpResult
{
Title = Title,
SubTitle = SubTitle,
ActionKeywordAssigned = ActionKeywordAssigned,
CopyText = CopyText,
AutoCompleteText = AutoCompleteText,
IcoPath = IcoPath,
BadgeIcoPath = BadgeIcoPath,
RoundedIcon = RoundedIcon,
Icon = Icon,
BadgeIcon = BadgeIcon,
Glyph = Glyph,
Action = Action,
AsyncAction = AsyncAction,
Score = Score,
TitleHighlightData = TitleHighlightData,
OriginQuery = OriginQuery,
PluginDirectory = PluginDirectory,
ContextData = ContextData,
PluginID = PluginID,
TitleToolTip = TitleToolTip,
SubTitleToolTip = SubTitleToolTip,
PreviewPanel = PreviewPanel,
ProgressBar = ProgressBar,
ProgressBarColor = ProgressBarColor,
Preview = Preview,
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey,
ShowBadge = ShowBadge,
DialogJumpPath = DialogJumpPath
};
}
/// <summary>
/// Convert <see cref="Result"/> to <see cref="DialogJumpResult"/>.
/// </summary>
public static DialogJumpResult From(Result result, string dialogJumpPath)
{
return new DialogJumpResult
{
Title = result.Title,
SubTitle = result.SubTitle,
ActionKeywordAssigned = result.ActionKeywordAssigned,
CopyText = result.CopyText,
AutoCompleteText = result.AutoCompleteText,
IcoPath = result.IcoPath,
BadgeIcoPath = result.BadgeIcoPath,
RoundedIcon = result.RoundedIcon,
Icon = result.Icon,
BadgeIcon = result.BadgeIcon,
Glyph = result.Glyph,
Action = result.Action,
AsyncAction = result.AsyncAction,
Score = result.Score,
TitleHighlightData = result.TitleHighlightData,
OriginQuery = result.OriginQuery,
PluginDirectory = result.PluginDirectory,
ContextData = result.ContextData,
PluginID = result.PluginID,
TitleToolTip = result.TitleToolTip,
SubTitleToolTip = result.SubTitleToolTip,
PreviewPanel = result.PreviewPanel,
ProgressBar = result.ProgressBar,
ProgressBarColor = result.ProgressBarColor,
Preview = result.Preview,
AddSelectedCount = result.AddSelectedCount,
RecordKey = result.RecordKey,
ShowBadge = result.ShowBadge,
DialogJumpPath = dialogJumpPath
};
}
}
}

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Asynchronous Dialog Jump Model
/// </summary>
public interface IAsyncDialogJump : IFeatures
{
/// <summary>
/// Asynchronous querying for Dialog Jump window
/// </summary>
/// <para>
/// If the Querying method requires high IO transmission
/// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncDialogJump interface
/// </para>
/// <param name="query">Query to search</param>
/// <param name="token">Cancel when querying job is obsolete</param>
/// <returns></returns>
Task<List<DialogJumpResult>> QueryDialogJumpAsync(Query query, CancellationToken token);
}
}

View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Synchronous Dialog Jump Model
/// <para>
/// If the Querying method requires high IO transmission
/// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncDialogJump interface
/// </para>
/// </summary>
public interface IDialogJump : IAsyncDialogJump
{
/// <summary>
/// Querying for Dialog Jump window
/// <para>
/// This method will be called within a Task.Run,
/// so please avoid synchrously wait for long.
/// </para>
/// </summary>
/// <param name="query">Query to search</param>
/// <returns></returns>
List<DialogJumpResult> QueryDialogJump(Query query);
Task<List<DialogJumpResult>> IAsyncDialogJump.QueryDialogJumpAsync(Query query, CancellationToken token) => Task.Run(() => QueryDialogJump(query), token);
}
}

View file

@ -0,0 +1,96 @@
using System;
#nullable enable
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Interface for handling file dialog instances in DialogJump.
/// </summary>
public interface IDialogJumpDialog : IFeatures, IDisposable
{
/// <summary>
/// Check if the foreground window is a file dialog instance.
/// </summary>
/// <param name="hwnd">
/// The handle of the foreground window to check.
/// </param>
/// <returns>
/// The window if the foreground window is a file dialog instance. Null if it is not.
/// </returns>
IDialogJumpDialogWindow? CheckDialogWindow(IntPtr hwnd);
}
/// <summary>
/// Interface for handling a specific file dialog window in DialogJump.
/// </summary>
public interface IDialogJumpDialogWindow : IDisposable
{
/// <summary>
/// The handle of the dialog window.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current tab of the dialog window.
/// </summary>
/// <returns></returns>
IDialogJumpDialogWindowTab GetCurrentTab();
}
/// <summary>
/// Interface for handling a specific tab in a file dialog window in DialogJump.
/// </summary>
public interface IDialogJumpDialogWindowTab : IDisposable
{
/// <summary>
/// The handle of the dialog tab.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current folder path of the dialog tab.
/// </summary>
/// <returns></returns>
string GetCurrentFolder();
/// <summary>
/// Get the current file of the dialog tab.
/// </summary>
/// <returns></returns>
string GetCurrentFile();
/// <summary>
/// Jump to a folder in the dialog tab.
/// </summary>
/// <param name="path">
/// The path to the folder to jump to.
/// </param>
/// <param name="auto">
/// Whether folder jump is under automatical mode.
/// </param>
/// <returns>
/// True if the jump was successful, false otherwise.
/// </returns>
bool JumpFolder(string path, bool auto);
/// <summary>
/// Jump to a file in the dialog tab.
/// </summary>
/// <param name="path">
/// The path to the file to jump to.
/// </param>
/// <returns>
/// True if the jump was successful, false otherwise.
/// </returns>
bool JumpFile(string path);
/// <summary>
/// Open the file in the dialog tab.
/// </summary>
/// <returns>
/// True if the file was opened successfully, false otherwise.
/// </returns>
bool Open();
}
}

View file

@ -0,0 +1,40 @@
using System;
#nullable enable
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Interface for handling file explorer instances in DialogJump.
/// </summary>
public interface IDialogJumpExplorer : IFeatures, IDisposable
{
/// <summary>
/// Check if the foreground window is a Windows Explorer instance.
/// </summary>
/// <param name="hwnd">
/// The handle of the foreground window to check.
/// </param>
/// <returns>
/// The window if the foreground window is a file explorer instance. Null if it is not.
/// </returns>
IDialogJumpExplorerWindow? CheckExplorerWindow(IntPtr hwnd);
}
/// <summary>
/// Interface for handling a specific file explorer window in DialogJump.
/// </summary>
public interface IDialogJumpExplorerWindow : IDisposable
{
/// <summary>
/// The handle of the explorer window.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current folder path of the explorer window.
/// </summary>
/// <returns></returns>
string? GetExplorerPath();
}
}

View file

@ -32,6 +32,6 @@ namespace Flow.Launcher.Plugin
Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context));
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query));
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query), token);
}
}

View file

@ -307,7 +307,7 @@ namespace Flow.Launcher.Plugin
Preview = Preview,
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey,
ShowBadge = ShowBadge,
ShowBadge = ShowBadge
};
}

View file

@ -16,6 +16,7 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -233,6 +234,9 @@ namespace Flow.Launcher
// Initialize theme for main window
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs());
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
RegisterExitEvents();
@ -412,6 +416,7 @@ namespace Flow.Launcher
// since some resources owned by the thread need to be disposed.
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
_mainVM?.Dispose();
DialogJump.Dispose();
}
API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------");

View file

@ -90,7 +90,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="MdXaml" Version="1.27.0" />
<PackageReference Include="MdXaml.AnimatedGif" Version="1.27.0" />
<PackageReference Include="MdXaml.Html" Version="1.27.0" />
@ -103,7 +102,6 @@
<!-- ModernWpfUI v0.9.5 introduced WinRT changes that causes Notification platform unavailable error on some machines -->
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -1,11 +1,12 @@
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.ViewModel;
using System;
using ChefKeys;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.ViewModel;
using NHotkey;
using NHotkey.Wpf;
namespace Flow.Launcher.Helper;
@ -22,6 +23,10 @@ internal static class HotKeyMapper
_settings = Ioc.Default.GetService<Settings>();
SetHotkey(_settings.Hotkey, OnToggleHotkey);
if (_settings.EnableDialogJump)
{
SetHotkey(_settings.DialogJumpHotkey, DialogJump.OnToggleHotkey);
}
LoadCustomPluginHotkey();
}

View file

@ -1,4 +1,4 @@
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
@ -110,7 +110,8 @@ namespace Flow.Launcher
SelectPrevItemHotkey,
SelectPrevItemHotkey2,
SelectNextItemHotkey,
SelectNextItemHotkey2
SelectNextItemHotkey2,
DialogJumpHotkey,
}
// We can initialize settings in static field because it has been constructed in App constuctor
@ -142,6 +143,7 @@ namespace Flow.Launcher
HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2,
HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey,
HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2,
HotkeyType.DialogJumpHotkey => _settings.DialogJumpHotkey,
_ => throw new System.NotImplementedException("Hotkey type not set")
};
}
@ -201,6 +203,9 @@ namespace Flow.Launcher
case HotkeyType.SelectNextItemHotkey2:
_settings.SelectNextItemHotkey2 = value;
break;
case HotkeyType.DialogJumpHotkey:
_settings.DialogJumpHotkey = value;
break;
default:
throw new System.NotImplementedException("Hotkey type not set");
}

View file

@ -385,6 +385,28 @@
<system:String x:Key="showBadges">Show Result Badges</system:String>
<system:String x:Key="showBadgesToolTip">For supported plugins, badges are displayed to help distinguish them more easily.</system:String>
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
<system:String x:Key="showBadgesGlobalOnlyToolTip">Show badges for global query results only</system:String>
<system:String x:Key="dialogJumpHotkey">Dialog Jump</system:String>
<system:String x:Key="dialogJumpHotkeyToolTip">Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager.</system:String>
<system:String x:Key="dialogJump">Dialog Jump</system:String>
<system:String x:Key="dialogJumpToolTip">When Open/Save As dialog window opens, quickly navigate to the current path of the file manager.</system:String>
<system:String x:Key="autoDialogJump">Dialog Jump Automatically</system:String>
<system:String x:Key="autoDialogJumpToolTip">When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental)</system:String>
<system:String x:Key="showDialogJumpWindow">Show Dialog Jump Window</system:String>
<system:String x:Key="showDialogJumpWindowToolTip">Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations.</system:String>
<system:String x:Key="dialogJumpWindowPosition">Dialog Jump Window Position</system:String>
<system:String x:Key="dialogJumpWindowPositionToolTip">Select position for the Dialog Jump search window</system:String>
<system:String x:Key="DialogJumpWindowPositionUnderDialog">Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed</system:String>
<system:String x:Key="DialogJumpWindowPositionFollowDefault">Default search window position. Displayed when triggered by search window hotkey</system:String>
<system:String x:Key="dialogJumpResultBehaviour">Dialog Jump Result Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window to the selected result path</system:String>
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Left click or Enter key</system:String>
<system:String x:Key="DialogJumpResultBehaviourRightClick">Right click</system:String>
<system:String x:Key="dialogJumpFileResultBehaviour">Dialog Jump File Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpFileResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window when the result is a file path</system:String>
<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>
<!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP Proxy</system:String>

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
using System.Linq;
using System.Media;
@ -19,6 +19,7 @@ using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -119,7 +120,7 @@ namespace Flow.Launcher
Win32Helper.DisableControlBox(this);
}
private void OnLoaded(object sender, RoutedEventArgs _)
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Check first launch
if (_settings.FirstLaunch)
@ -168,10 +169,12 @@ namespace Flow.Launcher
if (_settings.HideOnStartup)
{
_viewModel.Hide();
_viewModel.InitializeVisibilityStatus(false);
}
else
{
_viewModel.Show();
_viewModel.InitializeVisibilityStatus(true);
// When HideOnStartup is off and UseAnimation is on,
// there was a bug where the clock would not appear at all on the initial launch
// So we need to forcibly trigger animation here to ensure the clock is visible
@ -214,6 +217,9 @@ namespace Flow.Launcher
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
_viewModel.QueryTextCursorMovedToEnd = false;
// Register Dialog Jump events
InitializeDialogJump();
// View model property changed event
_viewModel.PropertyChanged += (o, e) =>
{
@ -226,7 +232,7 @@ namespace Flow.Launcher
if (_viewModel.MainWindowVisibilityStatus)
{
// Play sound effect before activing the window
if (_settings.UseSound)
if (_settings.UseSound && !_viewModel.IsDialogJumpWindowUnderDialog())
{
SoundPlay();
}
@ -249,7 +255,7 @@ namespace Flow.Launcher
QueryTextBox.Focus();
// Play window animation
if (_settings.UseAnimation)
if (_settings.UseAnimation && !_viewModel.IsDialogJumpWindowUnderDialog())
{
WindowAnimation();
}
@ -379,6 +385,11 @@ namespace Flow.Launcher
private void OnLocationChanged(object sender, EventArgs e)
{
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return;
}
if (IsLoaded)
{
_settings.WindowLeft = Left;
@ -388,6 +399,11 @@ namespace Flow.Launcher
private async void OnDeactivated(object sender, EventArgs e)
{
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return;
}
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
@ -577,11 +593,23 @@ namespace Flow.Launcher
switch (msg)
{
case Win32Helper.WM_ENTERSIZEMOVE:
// Do do handle size move event for dialog jump window
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return IntPtr.Zero;
}
_initialWidth = (int)Width;
_initialHeight = (int)Height;
handled = true;
break;
case Win32Helper.WM_EXITSIZEMOVE:
// Do do handle size move event for Dialog Jump window
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return IntPtr.Zero;
}
//Prevent updating the number of results when the window height is below the height of a single result item.
//This situation occurs not only when the user manually resizes the window, but also when the window is released from a side snap, as the OS automatically adjusts the window height.
//(Without this check, releasing from a snap can cause the window height to hit the minimum, resulting in only 2 results being shown.)
@ -792,11 +820,19 @@ namespace Flow.Launcher
#region Window Position
private void UpdatePosition()
public void UpdatePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
InitializePosition();
InitializePosition();
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
InitializeDialogJumpPosition();
}
else
{
InitializePosition();
InitializePosition();
}
}
private async Task PositionResetAsync()
@ -1354,6 +1390,46 @@ namespace Flow.Launcher
#endregion
#region Dialog Jump
private void InitializeDialogJump()
{
DialogJump.ShowDialogJumpWindowAsync = _viewModel.SetupDialogJumpAsync;
DialogJump.UpdateDialogJumpWindow = InitializeDialogJumpPosition;
DialogJump.ResetDialogJumpWindow = _viewModel.ResetDialogJump;
DialogJump.HideDialogJumpWindow = _viewModel.HideDialogJump;
}
private void InitializeDialogJumpPosition()
{
if (_viewModel.DialogWindowHandle == nint.Zero || !_viewModel.MainWindowVisibilityStatus) return;
if (!_viewModel.IsDialogJumpWindowUnderDialog()) return;
// Get dialog window rect
var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window);
if (!result) return;
// Move window below the bottom of the dialog and keep it center
Top = VerticalBottom(window);
Left = HorizonCenter(window);
}
private double HorizonCenter(Rect window)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, window.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, window.Width, 0);
var left = (dip2.X - ActualWidth) / 2 + dip1.X;
return left;
}
private double VerticalBottom(Rect window)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, window.Bottom);
return dip1.Y;
}
#endregion
#region IDisposable
protected virtual void Dispose(bool disposing)

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
@ -8,6 +8,7 @@ using Flow.Launcher.Core.Configuration;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@ -146,6 +147,40 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
public List<LastQueryModeData> LastQueryModes { get; } =
DropdownDataGeneric<LastQueryMode>.GetValues<LastQueryModeData>("LastQuery");
public bool EnableDialogJump
{
get => Settings.EnableDialogJump;
set
{
if (Settings.EnableDialogJump != value)
{
Settings.EnableDialogJump = value;
DialogJump.SetupDialogJump(value);
if (Settings.EnableDialogJump)
{
HotKeyMapper.SetHotkey(new(Settings.DialogJumpHotkey), DialogJump.OnToggleHotkey);
}
else
{
HotKeyMapper.RemoveHotkey(Settings.DialogJumpHotkey);
}
}
}
}
public class DialogJumpWindowPositionData : DropdownDataGeneric<DialogJumpWindowPositions> { }
public class DialogJumpResultBehaviourData : DropdownDataGeneric<DialogJumpResultBehaviours> { }
public class DialogJumpFileResultBehaviourData : DropdownDataGeneric<DialogJumpFileResultBehaviours> { }
public List<DialogJumpWindowPositionData> DialogJumpWindowPositions { get; } =
DropdownDataGeneric<DialogJumpWindowPositions>.GetValues<DialogJumpWindowPositionData>("DialogJumpWindowPosition");
public List<DialogJumpResultBehaviourData> DialogJumpResultBehaviours { get; } =
DropdownDataGeneric<DialogJumpResultBehaviours>.GetValues<DialogJumpResultBehaviourData>("DialogJumpResultBehaviour");
public List<DialogJumpFileResultBehaviourData> DialogJumpFileResultBehaviours { get; } =
DropdownDataGeneric<DialogJumpFileResultBehaviours>.GetValues<DialogJumpFileResultBehaviourData>("DialogJumpFileResultBehaviour");
public int SearchDelayTimeValue
{
get => Settings.SearchDelayTime;
@ -179,6 +214,9 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
DropdownDataGeneric<SearchPrecisionScore>.UpdateLabels(SearchPrecisionScores);
DropdownDataGeneric<LastQueryMode>.UpdateLabels(LastQueryModes);
DropdownDataGeneric<DoublePinyinSchemas>.UpdateLabels(DoublePinyinSchemas);
DropdownDataGeneric<DialogJumpWindowPositions>.UpdateLabels(DialogJumpWindowPositions);
DropdownDataGeneric<DialogJumpResultBehaviours>.UpdateLabels(DialogJumpResultBehaviours);
DropdownDataGeneric<DialogJumpFileResultBehaviours>.UpdateLabels(DialogJumpFileResultBehaviours);
// Since we are using Binding instead of DynamicResource, we need to manually trigger the update
OnPropertyChanged(nameof(AlwaysPreviewToolTip));
}

View file

@ -4,6 +4,7 @@ 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;
@ -34,6 +35,15 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey);
}
[RelayCommand]
private void SetDialogJumpHotkey(HotkeyModel hotkey)
{
if (Settings.EnableDialogJump)
{
HotKeyMapper.SetHotkey(hotkey, DialogJump.OnToggleHotkey);
}
}
[RelayCommand]
private void CustomHotkeyDelete()
{

View file

@ -1,4 +1,4 @@
<ui:Page
<ui:Page
x:Class="Flow.Launcher.SettingPages.Views.SettingsPaneGeneral"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -261,6 +261,80 @@
</cc:Card>
</cc:CardGroup>
<cc:ExCard
Title="{DynamicResource dialogJump}"
Margin="0 14 0 0"
Icon="&#xE8AB;"
Sub="{DynamicResource dialogJumpToolTip}">
<cc:ExCard.SideContent>
<ui:ToggleSwitch
IsOn="{Binding EnableDialogJump}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:ExCard.SideContent>
<StackPanel>
<cc:Card
Title="{DynamicResource autoDialogJump}"
Sub="{DynamicResource autoDialogJumpToolTip}"
Type="InsideFit"
Visibility="Collapsed">
<ui:ToggleSwitch
IsOn="{Binding Settings.AutoDialogJump}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource showDialogJumpWindow}"
Sub="{DynamicResource showDialogJumpWindowToolTip}"
Type="InsideFit">
<ui:ToggleSwitch
IsOn="{Binding Settings.ShowDialogJumpWindow}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource dialogJumpWindowPosition}"
Sub="{DynamicResource dialogJumpWindowPositionToolTip}"
Type="InsideFit">
<ComboBox
MinWidth="120"
MaxWidth="210"
DisplayMemberPath="Display"
ItemsSource="{Binding DialogJumpWindowPositions}"
SelectedValue="{Binding Settings.DialogJumpWindowPosition}"
SelectedValuePath="Value" />
</cc:Card>
<cc:Card
Title="{DynamicResource dialogJumpResultBehaviour}"
Sub="{DynamicResource dialogJumpResultBehaviourToolTip}"
Type="InsideFit">
<ComboBox
MinWidth="120"
DisplayMemberPath="Display"
ItemsSource="{Binding DialogJumpResultBehaviours}"
SelectedValue="{Binding Settings.DialogJumpResultBehaviour}"
SelectedValuePath="Value" />
</cc:Card>
<cc:Card
Title="{DynamicResource dialogJumpFileResultBehaviour}"
Sub="{DynamicResource dialogJumpFileResultBehaviourToolTip}"
Type="InsideFit">
<ComboBox
MinWidth="120"
MaxWidth="240"
DisplayMemberPath="Display"
ItemsSource="{Binding DialogJumpFileResultBehaviours}"
SelectedValue="{Binding Settings.DialogJumpFileResultBehaviour}"
SelectedValuePath="Value" />
</cc:Card>
</StackPanel>
</cc:ExCard>
<cc:ExCard
Title="{DynamicResource searchDelay}"
Margin="0 14 0 0"

View file

@ -73,6 +73,18 @@
</cc:Card>
</cc:CardGroup>
<cc:Card
Title="{DynamicResource dialogJumpHotkey}"
Margin="0 14 0 0"
Sub="{DynamicResource dialogJumpHotkeyToolTip}">
<flowlauncher:HotkeyControl
ChangeHotkey="{Binding SetDialogJumpHotkeyCommand}"
DefaultHotkey="Alt+G"
Type="DialogJumpHotkey"
ValidateKeyGesture="False"
WindowTitle="{DynamicResource dialogJumpHotkey}" />
</cc:Card>
<cc:ExCard
Title="{DynamicResource hotkeyPresets}"
Margin="0 14 0 0"

View file

@ -16,6 +16,7 @@ using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -52,6 +53,7 @@ namespace Flow.Launcher.ViewModel
private Task _resultsViewUpdateTask;
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
private readonly IReadOnlyList<DialogJumpResult> _emptyDialogJumpResult = new List<DialogJumpResult>();
private readonly PluginMetadata _historyMetadata = new()
{
@ -215,7 +217,8 @@ namespace Flow.Launcher.ViewModel
var resultUpdateChannel = Channel.CreateUnbounded<ResultsForUpdate>();
_resultsUpdateChannelWriter = resultUpdateChannel.Writer;
_resultsViewUpdateTask =
Task.Run(UpdateActionAsync).ContinueWith(continueAction, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
Task.Run(UpdateActionAsync).ContinueWith(continueAction,
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
async Task UpdateActionAsync()
{
@ -285,8 +288,16 @@ namespace Flow.Launcher.ViewModel
var token = e.Token == default ? _updateToken : e.Token;
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
var resultsCopy = DeepCloneResults(e.Results, token);
IReadOnlyList<Result> resultsCopy;
if (e.Results == null)
{
resultsCopy = _emptyResult;
}
else
{
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
resultsCopy = DeepCloneResults(e.Results, false, token);
}
foreach (var result in resultsCopy)
{
@ -394,12 +405,30 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void LoadContextMenu()
{
// For Dialog Jump and right click mode, we need to navigate to the path
if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.RightClick)
{
if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero)
{
var result = SelectedResults.SelectedItem.Result;
if (result is DialogJumpResult dialogJumpResult)
{
Win32Helper.SetForegroundWindow(DialogWindowHandle);
_ = Task.Run(() => DialogJump.JumpToPathAsync(DialogWindowHandle, dialogJumpResult.DialogJumpPath));
}
}
return;
}
// For query mode, we load context menu
if (QueryResultsSelected())
{
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
if (SelectedResults.SelectedItem != null)
{
SelectedResults = ContextMenu;
}
}
else
{
@ -469,12 +498,34 @@ namespace Flow.Launcher.ViewModel
return;
}
var hideWindow = await result.ExecuteAsync(new ActionContext
// For Dialog Jump and left click mode, we need to navigate to the path
if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.LeftClick)
{
// not null means pressing modifier key + number, should ignore the modifier key
SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers()
})
.ConfigureAwait(false);
Hide();
if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero)
{
if (result is DialogJumpResult dialogJumpResult)
{
Win32Helper.SetForegroundWindow(DialogWindowHandle);
_ = Task.Run(() => DialogJump.JumpToPathAsync(DialogWindowHandle, dialogJumpResult.DialogJumpPath));
}
}
}
// For query mode, we execute the result
else
{
var hideWindow = await result.ExecuteAsync(new ActionContext
{
// not null means pressing modifier key + number, should ignore the modifier key
SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers()
}).ConfigureAwait(false);
if (hideWindow)
{
Hide();
}
}
if (QueryResultsSelected())
{
@ -482,26 +533,33 @@ namespace Flow.Launcher.ViewModel
_history.Add(result.OriginQuery.RawQuery);
lastHistoryIndex = 1;
}
if (hideWindow)
{
Hide();
}
}
private static IReadOnlyList<Result> DeepCloneResults(IReadOnlyList<Result> results, CancellationToken token = default)
private static IReadOnlyList<Result> DeepCloneResults(IReadOnlyList<Result> results, bool isDialogJump, CancellationToken token = default)
{
var resultsCopy = new List<Result>();
foreach (var result in results.ToList())
{
if (token.IsCancellationRequested)
{
break;
}
var resultCopy = result.Clone();
resultsCopy.Add(resultCopy);
if (isDialogJump)
{
foreach (var result in results.ToList())
{
if (token.IsCancellationRequested) break;
var resultCopy = ((DialogJumpResult)result).Clone();
resultsCopy.Add(resultCopy);
}
}
else
{
foreach (var result in results.ToList())
{
if (token.IsCancellationRequested) break;
var resultCopy = result.Clone();
resultsCopy.Add(resultCopy);
}
}
return resultsCopy;
}
@ -1279,25 +1337,21 @@ namespace Flow.Launcher.ViewModel
if (query == null) // shortcut expanded
{
App.API.LogDebug(ClassName, $"Clear query results");
// Hide and clear results again because running query may show and add some results
Results.Visibility = Visibility.Collapsed;
Results.Clear();
// Reset plugin icon
PluginIconPath = null;
PluginIconSource = null;
SearchIconVisibility = Visibility.Visible;
// Hide progress bar again because running query may set this to visible
ProgressBarVisibility = Visibility.Hidden;
ClearResults();
return;
}
App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>");
var currentIsHomeQuery = query.IsHomeQuery;
var currentIsDialogJump = _isDialogJump;
// Do not show home page for Dialog Jump window
if (currentIsHomeQuery && currentIsDialogJump)
{
ClearResults();
return;
}
_updateSource?.Dispose();
@ -1331,7 +1385,7 @@ namespace Flow.Launcher.ViewModel
}
else
{
plugins = PluginManager.ValidPluginsForQuery(query);
plugins = PluginManager.ValidPluginsForQuery(query, currentIsDialogJump);
if (plugins.Count == 1)
{
@ -1425,6 +1479,23 @@ namespace Flow.Launcher.ViewModel
}
// Local function
void ClearResults()
{
App.API.LogDebug(ClassName, $"Clear query results");
// Hide and clear results again because running query may show and add some results
Results.Visibility = Visibility.Collapsed;
Results.Clear();
// Reset plugin icon
PluginIconPath = null;
PluginIconSource = null;
SearchIconVisibility = Visibility.Visible;
// Hide progress bar again because running query may set this to visible
ProgressBarVisibility = Visibility.Hidden;
}
async Task QueryTaskAsync(PluginPair plugin, CancellationToken token)
{
App.API.LogDebug(ClassName, $"Wait for querying plugin <{plugin.Metadata.Name}>");
@ -1442,21 +1513,23 @@ namespace Flow.Launcher.ViewModel
// Task.Yield will force it to run in ThreadPool
await Task.Yield();
var results = currentIsHomeQuery ?
await PluginManager.QueryHomeForPluginAsync(plugin, query, token) :
await PluginManager.QueryForPluginAsync(plugin, query, token);
IReadOnlyList<Result> results = currentIsDialogJump ?
await PluginManager.QueryDialogJumpForPluginAsync(plugin, query, token) :
currentIsHomeQuery ?
await PluginManager.QueryHomeForPluginAsync(plugin, query, token) :
await PluginManager.QueryForPluginAsync(plugin, query, token);
if (token.IsCancellationRequested) return;
IReadOnlyList<Result> resultsCopy;
if (results == null)
{
resultsCopy = _emptyResult;
resultsCopy = currentIsDialogJump ? _emptyDialogJumpResult : _emptyResult;
}
else
{
// make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc.
resultsCopy = DeepCloneResults(results, token);
resultsCopy = DeepCloneResults(results, currentIsDialogJump, token);
}
foreach (var result in resultsCopy)
@ -1751,6 +1824,208 @@ namespace Flow.Launcher.ViewModel
#endregion
#region Dialog Jump
public nint DialogWindowHandle { get; private set; } = nint.Zero;
private bool _isDialogJump = false;
private bool _previousMainWindowVisibilityStatus;
private CancellationTokenSource _dialogJumpSource;
public void InitializeVisibilityStatus(bool visibilityStatus)
{
_previousMainWindowVisibilityStatus = visibilityStatus;
}
public bool IsDialogJumpWindowUnderDialog()
{
return _isDialogJump && DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog;
}
public async Task SetupDialogJumpAsync(nint handle)
{
if (handle == nint.Zero) return;
// Only set flag & reset window once for one file dialog
var dialogWindowHandleChanged = false;
if (DialogWindowHandle != handle)
{
DialogWindowHandle = handle;
_previousMainWindowVisibilityStatus = MainWindowVisibilityStatus;
_isDialogJump = true;
dialogWindowHandleChanged = true;
// If don't give a time, Positioning will be weird
await Task.Delay(300);
}
// If handle is cleared, which means the dialog is closed, clear Dialog Jump state
if (DialogWindowHandle == nint.Zero)
{
_isDialogJump = false;
return;
}
// Initialize Dialog Jump window
if (MainWindowVisibilityStatus)
{
if (dialogWindowHandleChanged)
{
// Only update the position
Application.Current?.Dispatcher.Invoke(() =>
{
(Application.Current?.MainWindow as MainWindow)?.UpdatePosition();
});
_ = ResetWindowAsync();
}
}
else
{
if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog)
{
// We wait for window to be reset before showing it because if window has results,
// showing it before resetting will cause flickering when results are clearing
if (dialogWindowHandleChanged)
{
await ResetWindowAsync();
}
Show();
}
else
{
if (dialogWindowHandleChanged)
{
_ = ResetWindowAsync();
}
}
}
if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog)
{
// Cancel the previous Dialog Jump task
_dialogJumpSource?.Cancel();
// Create a new cancellation token source
_dialogJumpSource = new CancellationTokenSource();
_ = Task.Run(() =>
{
try
{
// Check task cancellation
if (_dialogJumpSource.Token.IsCancellationRequested) return;
// Check dialog handle
if (DialogWindowHandle == nint.Zero) return;
// Wait 150ms to check if Dialog Jump window gets the focus
var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150);
if (timeOut) return;
// Bring focus back to the dialog
Win32Helper.SetForegroundWindow(DialogWindowHandle);
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to focus on dialog window", e);
}
});
}
}
#pragma warning disable VSTHRD100 // Avoid async void methods
public async void ResetDialogJump()
{
// Cache original dialog window handle
var dialogWindowHandle = DialogWindowHandle;
// Reset the Dialog Jump state
DialogWindowHandle = nint.Zero;
_isDialogJump = false;
// If dialog window handle is not set, we should not reset the main window visibility
if (dialogWindowHandle == nint.Zero) return;
if (_previousMainWindowVisibilityStatus != MainWindowVisibilityStatus)
{
// We wait for window to be reset before showing it because if window has results,
// showing it before resetting will cause flickering when results are clearing
await ResetWindowAsync();
// Show or hide to change visibility
if (_previousMainWindowVisibilityStatus)
{
Show();
}
else
{
Hide(false);
}
}
else
{
if (_previousMainWindowVisibilityStatus)
{
// Only update the position
Application.Current?.Dispatcher.Invoke(() =>
{
(Application.Current?.MainWindow as MainWindow)?.UpdatePosition();
});
_ = ResetWindowAsync();
}
else
{
_ = ResetWindowAsync();
}
}
}
#pragma warning restore VSTHRD100 // Avoid async void methods
public void HideDialogJump()
{
if (DialogWindowHandle != nint.Zero)
{
if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog)
{
// Warning: Main window is already in foreground
// This is because if you click popup menus in other applications to hide Dialog Jump window,
// they can steal focus before showing main window
if (MainWindowVisibilityStatus)
{
Hide();
}
}
}
}
// Reset index & preview & selected results & query text
private async Task ResetWindowAsync()
{
lastHistoryIndex = 1;
if (ExternalPreviewVisible)
{
await CloseExternalPreviewAsync();
}
if (!QueryResultsSelected())
{
SelectedResults = Results;
}
await ChangeQueryTextAsync(string.Empty, true);
}
#endregion
#region Public Methods
#pragma warning disable VSTHRD100 // Avoid async void methods
@ -1770,7 +2045,7 @@ namespace Flow.Launcher.ViewModel
Win32Helper.DWMSetCloakForWindow(mainWindow, false);
// Set clock and search icon opacity
var opacity = Settings.UseAnimation ? 0.0 : 1.0;
var opacity = (Settings.UseAnimation && !_isDialogJump) ? 0.0 : 1.0;
ClockPanelOpacity = opacity;
SearchIconOpacity = opacity;
@ -1799,37 +2074,40 @@ namespace Flow.Launcher.ViewModel
}
}
public async void Hide()
public async void Hide(bool reset = true)
{
lastHistoryIndex = 1;
if (ExternalPreviewVisible)
if (reset)
{
await CloseExternalPreviewAsync();
}
lastHistoryIndex = 1;
BackToQueryResults();
if (ExternalPreviewVisible)
{
await CloseExternalPreviewAsync();
}
switch (Settings.LastQueryMode)
{
case LastQueryMode.Empty:
await ChangeQueryTextAsync(string.Empty);
break;
case LastQueryMode.Preserved:
case LastQueryMode.Selected:
LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved;
break;
case LastQueryMode.ActionKeywordPreserved:
case LastQueryMode.ActionKeywordSelected:
var newQuery = _lastQuery?.ActionKeyword;
BackToQueryResults();
if (!string.IsNullOrEmpty(newQuery))
newQuery += " ";
await ChangeQueryTextAsync(newQuery);
switch (Settings.LastQueryMode)
{
case LastQueryMode.Empty:
await ChangeQueryTextAsync(string.Empty);
break;
case LastQueryMode.Preserved:
case LastQueryMode.Selected:
LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved;
break;
case LastQueryMode.ActionKeywordPreserved:
case LastQueryMode.ActionKeywordSelected:
var newQuery = _lastQuery.ActionKeyword;
if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected)
LastQuerySelected = false;
break;
if (!string.IsNullOrEmpty(newQuery))
newQuery += " ";
await ChangeQueryTextAsync(newQuery);
if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected)
LastQuerySelected = false;
break;
}
}
// When application is exiting, the Application.Current will be null
@ -1839,7 +2117,7 @@ namespace Flow.Launcher.ViewModel
if (Application.Current?.MainWindow is MainWindow mainWindow)
{
// Set clock and search icon opacity
var opacity = Settings.UseAnimation ? 0.0 : 1.0;
var opacity = (Settings.UseAnimation && !_isDialogJump) ? 0.0 : 1.0;
ClockPanelOpacity = opacity;
SearchIconOpacity = opacity;
@ -1984,6 +2262,7 @@ namespace Flow.Launcher.ViewModel
if (disposing)
{
_updateSource?.Dispose();
_dialogJumpSource?.Dispose();
_resultsUpdateChannelWriter?.Complete();
if (_resultsViewUpdateTask?.IsCompleted == true)
{

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin.Explorer.Helper;
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;
@ -10,10 +10,11 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using Flow.Launcher.Plugin.Explorer.Exceptions;
using System.Linq;
namespace Flow.Launcher.Plugin.Explorer
{
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncDialogJump
{
internal static PluginInitContext Context { get; set; }
@ -25,6 +26,8 @@ namespace Flow.Launcher.Plugin.Explorer
private SearchManager searchManager;
private static readonly List<DialogJumpResult> _emptyDialogJumpResultList = new();
public Control CreateSettingPanel()
{
return new ExplorerSettings(viewModel);
@ -108,5 +111,18 @@ namespace Flow.Launcher.Plugin.Explorer
}
}
}
public async Task<List<DialogJumpResult>> QueryDialogJumpAsync(Query query, CancellationToken token)
{
try
{
var results = await searchManager.SearchAsync(query, token);
return results.Select(r => DialogJumpResult.From(r, r.CopyText)).ToList();
}
catch (Exception e) when (e is SearchException or EngineNotAvailableException)
{
return _emptyDialogJumpResultList;
}
}
}
}

View file

@ -283,15 +283,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false)
{
bool isMedia = IsMedia(Path.GetExtension(filePath));
var title = Path.GetFileName(filePath);
var isMedia = IsMedia(Path.GetExtension(filePath));
var title = Path.GetFileName(filePath) ?? string.Empty;
var directory = Path.GetDirectoryName(filePath) ?? string.Empty;
/* Preview Detail */
var result = new Result
{
Title = title,
SubTitle = Path.GetDirectoryName(filePath),
SubTitle = directory,
IcoPath = filePath,
Preview = new Result.PreviewInfo
{
@ -315,7 +316,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift))
{
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, true);
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty, true);
}
else if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control)
{
@ -323,7 +324,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}
else
{
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty);
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty);
}
}
catch (Exception ex)