mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Dialog Jump - Quickly navigate the Open/Save As dialog window (#1018)
This commit is contained in:
parent
ff2d5e89f9
commit
4f269d3fa9
29 changed files with 2877 additions and 103 deletions
|
|
@ -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
|
||||
|
|
|
|||
1079
Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
Normal file
1079
Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
Normal file
File diff suppressed because it is too large
Load diff
63
Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs
Normal file
63
Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
345
Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs
Normal file
345
Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
92
Flow.Launcher.Plugin/DialogJumpResult.cs
Normal file
92
Flow.Launcher.Plugin/DialogJumpResult.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs
Normal file
24
Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
29
Flow.Launcher.Plugin/Interfaces/IDialogJump.cs
Normal file
29
Flow.Launcher.Plugin/Interfaces/IDialogJump.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
96
Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs
Normal file
96
Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
40
Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs
Normal file
40
Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ namespace Flow.Launcher.Plugin
|
|||
Preview = Preview,
|
||||
AddSelectedCount = AddSelectedCount,
|
||||
RecordKey = RecordKey,
|
||||
ShowBadge = ShowBadge,
|
||||
ShowBadge = ShowBadge
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ----------------------------------------------------");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue