Skip to content

Commit 6b7a70a

Browse files
Jack251970idkidknow
andcommitted
Implement window dialogs & explorer
Co-authored-by: idkidknow <[email protected]>
1 parent cfbfb7b commit 6b7a70a

File tree

2 files changed

+522
-0
lines changed

2 files changed

+522
-0
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
using System;
2+
using System.Threading;
3+
using Flow.Launcher.Infrastructure.Logger;
4+
using Flow.Launcher.Infrastructure.QuickSwitch.Interface;
5+
using Windows.Win32;
6+
using Windows.Win32.Foundation;
7+
using Windows.Win32.UI.WindowsAndMessaging;
8+
using WindowsInput;
9+
using WindowsInput.Native;
10+
11+
namespace Flow.Launcher.Infrastructure.QuickSwitch.Models
12+
{
13+
/// <summary>
14+
/// Class for handling Windows File Dialog instances in QuickSwitch.
15+
/// </summary>
16+
internal class WindowsDialog : IQuickSwitchDialog
17+
{
18+
public IQuickSwitchDialogWindow DialogWindow { get; private set; }
19+
20+
private const string WindowsDialogClassName = "#32770";
21+
22+
public bool CheckDialogWindow(HWND hwnd)
23+
{
24+
// Has it been checked?
25+
if (DialogWindow != null && DialogWindow.Handle == hwnd)
26+
{
27+
return true;
28+
}
29+
30+
// Is it a Win32 dialog box?
31+
if (GetClassName(hwnd) == WindowsDialogClassName)
32+
{
33+
// Is it a windows file dialog?
34+
var dialogType = GetFileDialogType(hwnd);
35+
if (dialogType != DialogType.Others)
36+
{
37+
DialogWindow = new WindowsDialogWindow(hwnd, dialogType);
38+
39+
return true;
40+
}
41+
}
42+
return false;
43+
}
44+
45+
public void Dispose()
46+
{
47+
DialogWindow?.Dispose();
48+
DialogWindow = null;
49+
}
50+
51+
#region Help Methods
52+
53+
private static unsafe string GetClassName(HWND handle)
54+
{
55+
fixed (char* buf = new char[256])
56+
{
57+
return PInvoke.GetClassName(handle, buf, 256) switch
58+
{
59+
0 => string.Empty,
60+
_ => new string(buf),
61+
};
62+
}
63+
}
64+
65+
private static DialogType GetFileDialogType(HWND handle)
66+
{
67+
// Is it a Windows Open file dialog?
68+
var fileEditor = PInvoke.GetDlgItem(handle, 0x047C);
69+
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open;
70+
71+
// Is it a Windows Save or Save As file dialog?
72+
fileEditor = PInvoke.GetDlgItem(handle, 0x0000);
73+
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs;
74+
75+
return DialogType.Others;
76+
}
77+
78+
#endregion
79+
}
80+
81+
internal class WindowsDialogWindow : IQuickSwitchDialogWindow
82+
{
83+
public HWND Handle { get; private set; } = HWND.Null;
84+
85+
// After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore
86+
// So we need to cache the current tab and use the original handle
87+
private IQuickSwitchDialogWindowTab _currentTab { get; set; } = null;
88+
89+
private readonly DialogType _dialogType;
90+
91+
public WindowsDialogWindow(HWND handle, DialogType dialogType)
92+
{
93+
Handle = handle;
94+
_dialogType = dialogType;
95+
}
96+
97+
public IQuickSwitchDialogWindowTab GetCurrentTab()
98+
{
99+
return _currentTab ??= new WindowsDialogTab(Handle, _dialogType);
100+
}
101+
102+
public void Dispose()
103+
{
104+
Handle = HWND.Null;
105+
}
106+
}
107+
108+
internal class WindowsDialogTab : IQuickSwitchDialogWindowTab
109+
{
110+
#region Public Properties
111+
112+
public HWND Handle { get; private set; } = HWND.Null;
113+
114+
#endregion
115+
116+
#region Private Fields
117+
118+
private static readonly string ClassName = nameof(WindowsDialogTab);
119+
120+
private static readonly InputSimulator _inputSimulator = new();
121+
122+
private readonly DialogType _dialogType;
123+
124+
private bool _legacy { get; set; } = false;
125+
private HWND _pathControl { get; set; } = HWND.Null;
126+
private HWND _pathEditor { get; set; } = HWND.Null;
127+
private HWND _fileEditor { get; set; } = HWND.Null;
128+
private HWND _openButton { get; set; } = HWND.Null;
129+
130+
#endregion
131+
132+
#region Constructor
133+
134+
public WindowsDialogTab(HWND handle, DialogType dialogType)
135+
{
136+
Handle = handle;
137+
_dialogType = dialogType;
138+
Log.Debug(ClassName, $"File dialog type: {dialogType}");
139+
}
140+
141+
#endregion
142+
143+
#region Public Methods
144+
145+
public string GetCurrentFolder()
146+
{
147+
if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty;
148+
return GetWindowText(_pathEditor);
149+
}
150+
151+
public string GetCurrentFile()
152+
{
153+
if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty;
154+
return GetWindowText(_fileEditor);
155+
}
156+
157+
public bool JumpFolder(string path, bool auto)
158+
{
159+
if (auto)
160+
{
161+
// Use legacy jump folder method for auto quick switch because file editor is default value.
162+
// After setting path using file editor, we do not need to revert its value.
163+
return JumpFolderWithFileEditor(path, false);
164+
}
165+
166+
// Alt-D or Ctrl-L to focus on the path input box
167+
// "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus
168+
_inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D);
169+
// _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L);
170+
171+
if (_pathControl.IsNull && !GetPathControlEditor())
172+
{
173+
// https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1
174+
// The dialog is a legacy one, so we can only edit file editor directly.
175+
Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method");
176+
return JumpFolderWithFileEditor(path, true);
177+
}
178+
179+
var timeOut = !SpinWait.SpinUntil(() =>
180+
{
181+
var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
182+
return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0;
183+
}, 1000);
184+
if (timeOut)
185+
{
186+
// Path control is not visible, so we can only edit file editor directly.
187+
Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method");
188+
return JumpFolderWithFileEditor(path, true);
189+
}
190+
191+
if (_pathEditor.IsNull)
192+
{
193+
// Path editor cannot be found, so we can only edit file editor directly.
194+
Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method");
195+
return JumpFolderWithFileEditor(path, true);
196+
}
197+
SetWindowText(_pathEditor, path);
198+
199+
_inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN);
200+
201+
return true;
202+
}
203+
204+
public bool JumpFile(string path)
205+
{
206+
if (_fileEditor.IsNull && !GetFileEditor()) return false;
207+
SetWindowText(_fileEditor, path);
208+
209+
return true;
210+
}
211+
212+
public bool Open()
213+
{
214+
if (_openButton.IsNull && !GetOpenButton()) return false;
215+
PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
216+
217+
return true;
218+
}
219+
220+
public void Dispose()
221+
{
222+
Handle = HWND.Null;
223+
}
224+
225+
#endregion
226+
227+
#region Helper Methods
228+
229+
#region Get Handles
230+
231+
private bool GetPathControlEditor()
232+
{
233+
// Get the handle of the path editor
234+
// Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control
235+
_pathControl = PInvoke.FindWindowEx(Handle, HWND.Null, "WorkerW", null); // 0x0000
236+
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005
237+
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205
238+
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000
239+
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205
240+
if (_pathControl == HWND.Null)
241+
{
242+
_pathEditor = HWND.Null;
243+
_legacy = true;
244+
Log.Info(ClassName, "Legacy dialog");
245+
}
246+
else
247+
{
248+
_pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox
249+
_pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit
250+
if (_pathEditor == HWND.Null)
251+
{
252+
_legacy = true;
253+
Log.Error(ClassName, "Failed to find path editor handle");
254+
}
255+
}
256+
257+
return !_legacy;
258+
}
259+
260+
private bool GetFileEditor()
261+
{
262+
if (_dialogType == DialogType.Open)
263+
{
264+
// Get the handle of the file name editor of Open file dialog
265+
_fileEditor = PInvoke.GetDlgItem(Handle, 0x047C); // ComboBoxEx32
266+
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox
267+
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit
268+
}
269+
else
270+
{
271+
// Get the handle of the file name editor of Save / SaveAs file dialog
272+
_fileEditor = PInvoke.GetDlgItem(Handle, 0x0000); // DUIViewWndClassName
273+
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND
274+
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink
275+
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox
276+
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit
277+
}
278+
279+
if (_fileEditor == HWND.Null)
280+
{
281+
Log.Error(ClassName, "Failed to find file name editor handle");
282+
return false;
283+
}
284+
285+
return true;
286+
}
287+
288+
private bool GetOpenButton()
289+
{
290+
// Get the handle of the open button
291+
_openButton = PInvoke.GetDlgItem(Handle, 0x0001); // Open/Save/SaveAs Button
292+
if (_openButton == HWND.Null)
293+
{
294+
Log.Error(ClassName, "Failed to find open button handle");
295+
return false;
296+
}
297+
298+
return true;
299+
}
300+
301+
#endregion
302+
303+
#region Windows Text
304+
305+
private static unsafe string GetWindowText(HWND handle)
306+
{
307+
int length;
308+
Span<char> buffer = stackalloc char[1000];
309+
fixed (char* pBuffer = buffer)
310+
{
311+
// If the control has no title bar or text, or if the control handle is invalid, the return value is zero.
312+
length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer);
313+
}
314+
315+
return buffer[..length].ToString();
316+
}
317+
318+
private static unsafe nint SetWindowText(HWND handle, string text)
319+
{
320+
fixed (char* textPtr = text + '\0')
321+
{
322+
return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value;
323+
}
324+
}
325+
326+
#endregion
327+
328+
#region Legacy Jump Folder
329+
330+
private bool JumpFolderWithFileEditor(string path, bool resetFocus)
331+
{
332+
// For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors.
333+
if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false;
334+
335+
if (_fileEditor.IsNull && !GetFileEditor()) return false;
336+
SetWindowText(_fileEditor, path);
337+
338+
if (_openButton.IsNull && !GetOpenButton()) return false;
339+
PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
340+
341+
return true;
342+
}
343+
344+
#endregion
345+
346+
#endregion
347+
}
348+
349+
internal enum DialogType
350+
{
351+
Others,
352+
Open,
353+
SaveOrSaveAs
354+
}
355+
}

0 commit comments

Comments
 (0)