Skip to content

Commit

Permalink
Fix file dialog "Append extension" checkbox not working in empty dir
Browse files Browse the repository at this point in the history
Use hooks for keyboard and window procedure instead of subclassing controls.
Use a handle map for transferring instance data to the hook procedure.
This approach should be more reliable than the previous one.

Fix #10436, close #11050
  • Loading branch information
mere-human authored and donho committed Feb 7, 2022
1 parent bbe8a7d commit b5a5baf
Showing 1 changed file with 111 additions and 89 deletions.
200 changes: 111 additions & 89 deletions PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#endif
#include <comdef.h> // _com_error
#include <comip.h> // _com_ptr_t
#include <unordered_map>
#include "CustomFileDialog.h"
#include "Parameters.h"

Expand Down Expand Up @@ -171,13 +172,16 @@ namespace // anonymous
return {};
}

LRESULT callWindowClassProc(const TCHAR* className, HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
HWND getDialogHandle(IFileDialog* dialog)
{
WNDCLASSEX wndclass = {};
wndclass.cbSize = sizeof(wndclass);
if (GetClassInfoEx(nullptr, className, &wndclass) && wndclass.lpfnWndProc)
return CallWindowProc(wndclass.lpfnWndProc, hwnd, msg, wparam, lparam);
return FALSE;
com_ptr<IOleWindow> pOleWnd = dialog;
if (pOleWnd)
{
HWND hwnd = nullptr;
if (SUCCEEDED(pOleWnd->GetWindow(&hwnd)))
return hwnd;
}
return nullptr;
}

// Backs up the current directory in constructor and restores it in destructor.
Expand Down Expand Up @@ -251,21 +255,22 @@ class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogContr
}
IFACEMETHODIMP OnFolderChange(IFileDialog*) override
{
// First launch order: 3. Custom controls are added but inactive.
// Dialog startup calling order: 3. Custom controls are added but inactive.
if (!foundControls())
findControls();
return S_OK;
}
IFACEMETHODIMP OnFolderChanging(IFileDialog*, IShellItem* psi) override
{
// Called when the current dialog folder is about to change.
// First launch order: 2. Buttons are added, correct window title.
// Dialog startup calling order: 2. Buttons are added, correct window title.
_lastUsedFolder = getFilename(psi);
return S_OK;
}
IFACEMETHODIMP OnSelectionChange(IFileDialog*) override
{
// First launch order: 4. Main window is shown.
if (shouldInitControls())
initControls();
// This event isn't triggered in an empty folder.
// Dialog startup calling order: 4. Main window is shown.
return S_OK;
}
IFACEMETHODIMP OnShareViolation(IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) override
Expand All @@ -274,7 +279,7 @@ class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogContr
}
IFACEMETHODIMP OnTypeChange(IFileDialog*) override
{
// First launch order: 1. Inactive, window title might be wrong.
// Dialog startup calling order: 1. Inactive, window title might be wrong.
UINT dialogIndex = 0;
if (SUCCEEDED(_dialog->GetFileTypeIndex(&dialogIndex)))
{
Expand Down Expand Up @@ -343,15 +348,17 @@ class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogContr
return E_NOTIMPL;
}


FileDialogEventHandler(IFileDialog* dlg, const std::vector<Filter>& filterSpec, int fileIndex, int wildcardIndex)
: _cRef(1), _dialog(dlg), _customize(dlg), _filterSpec(filterSpec), _currentType(fileIndex + 1),
_lastSelectedType(fileIndex + 1), _wildcardType(wildcardIndex >= 0 ? wildcardIndex + 1 : 0)
{
installHooks();
}

~FileDialogEventHandler()
{
eraseHandles();
removeHooks();
}

const generic_string& getLastUsedFolder() const { return _lastUsedFolder; }
Expand All @@ -362,31 +369,72 @@ class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogContr
FileDialogEventHandler(FileDialogEventHandler&&) = delete;
FileDialogEventHandler& operator=(FileDialogEventHandler&&) = delete;

// Overrides window procedures for file name edit and ok button.
// Find window handles for file name edit and ok button.
// Call this as late as possible to ensure all the controls of the dialog are created.
void initControls()
bool findControls()
{
assert(_dialog);
com_ptr<IOleWindow> pOleWnd = _dialog;
if (pOleWnd)
HWND hwndDlg = getDialogHandle(_dialog);
if (hwndDlg)
{
HWND hwndDlg = nullptr;
HRESULT hr = pOleWnd->GetWindow(&hwndDlg);
if (SUCCEEDED(hr) && hwndDlg)
{
EnumChildWindows(hwndDlg, &EnumChildProc, reinterpret_cast<LPARAM>(this));
if (_hwndButton && !GetWindowLongPtr(_hwndButton, GWLP_USERDATA))
{
SetWindowLongPtr(_hwndButton, GWLP_USERDATA, reinterpret_cast<LPARAM>(this));
_okButtonProc = (WNDPROC)SetWindowLongPtr(_hwndButton, GWLP_WNDPROC, (LPARAM)&OkButtonWndProc);
}
}
EnumChildWindows(hwndDlg, &EnumChildProc, reinterpret_cast<LPARAM>(this));
if (_hwndButton)
s_handleMap[_hwndButton] = this;
if (_hwndNameEdit)
s_handleMap[_hwndNameEdit] = this;
}
return foundControls();
}

bool shouldInitControls() const
bool foundControls() const
{
return !_okButtonProc && !_fileNameProc;
return _hwndButton && _hwndNameEdit;
}

void installHooks()
{
_prevKbdHook = ::SetWindowsHookEx(WH_KEYBOARD,
reinterpret_cast<HOOKPROC>(&FileDialogEventHandler::KbdProcHook),
nullptr,
::GetCurrentThreadId()
);
_prevCallHook = ::SetWindowsHookEx(WH_CALLWNDPROC,
reinterpret_cast<HOOKPROC>(&FileDialogEventHandler::CallProcHook),
nullptr,
::GetCurrentThreadId()
);
}

void removeHooks()
{
if (_prevKbdHook)
::UnhookWindowsHookEx(_prevKbdHook);
if (_prevCallHook)
::UnhookWindowsHookEx(_prevCallHook);
_prevKbdHook = nullptr;
_prevCallHook = nullptr;
}

void eraseHandles()
{
if (_hwndButton && _hwndNameEdit)
{
s_handleMap.erase(_hwndButton);
s_handleMap.erase(_hwndNameEdit);
}
else
{
std::vector<HWND> handlesToErase;
for (auto&& x : s_handleMap)
{
if (x.second == this)
handlesToErase.push_back(x.first);
}
for (auto&& h : handlesToErase)
{
s_handleMap.erase(h);
}
}
}

bool changeExt(generic_string& name, int extIndex)
Expand Down Expand Up @@ -460,7 +508,7 @@ class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogContr
}

// Enumerates the child windows of a dialog.
// Sets up window procedure overrides for "OK" button and file name edit box.
// Remember handles of "OK" button and file name edit box.
static BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM param)
{
const int bufferLen = MAX_PATH;
Expand All @@ -478,10 +526,8 @@ class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogContr
// The edit box of interest is a child of the combo box and has empty window text.
// We use the first combo box, but there might be the others (file type dropdown, address bar, etc).
HWND hwndChild = FindWindowEx(hwnd, nullptr, _T("Edit"), _T(""));
if (hwndChild && !inst->_hwndNameEdit && !GetWindowLongPtr(hwndChild, GWLP_USERDATA))
if (hwndChild && !inst->_hwndNameEdit)
{
SetWindowLongPtr(hwndChild, GWLP_USERDATA, reinterpret_cast<LPARAM>(inst));
inst->_fileNameProc = (WNDPROC)SetWindowLongPtr(hwndChild, GWLP_WNDPROC, reinterpret_cast<LPARAM>(&FileNameWndProc));
inst->_hwndNameEdit = hwndChild;
}
}
Expand Down Expand Up @@ -532,84 +578,60 @@ class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogContr
return TRUE;
}

static LRESULT CALLBACK OkButtonWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
static LRESULT CALLBACK CallProcHook(int nCode, WPARAM wParam, LPARAM lParam)
{
// The ways to press a button:
// 1. space/enter is pressed when the button has focus (WM_KEYDOWN)
// 2. left mouse click on a button (WM_LBUTTONDOWN)
// 3. Alt + S
bool pressed = false;
switch (msg)
{
case BM_SETSTATE:
// Sent after all press events above except when press return while focused.
pressed = (wparam == TRUE);
break;
case WM_GETDLGCODE:
// Sent for the keyboard input.
pressed = (wparam == VK_RETURN);
break;
}
auto* inst = reinterpret_cast<FileDialogEventHandler*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
if (inst)
if (nCode == HC_ACTION)
{
if (pressed)
inst->onPreFileOk();
return CallWindowProc(inst->_okButtonProc, hwnd, msg, wparam, lparam);
auto* msg = reinterpret_cast<CWPSTRUCT*>(lParam);
if (msg && msg->message == WM_COMMAND && HIWORD(msg->wParam) == BN_CLICKED)
{
// Handle Save/Open button click.
// Different ways to press a button:
// 1. space/enter is pressed when the button has focus (WM_KEYDOWN)
// 2. left mouse click on a button (WM_LBUTTONDOWN)
// 3. Alt + S
auto ctrlId = LOWORD(msg->wParam);
HWND hwnd = GetDlgItem(msg->hwnd, ctrlId);
auto it = s_handleMap.find(hwnd);
if (it != s_handleMap.end() && it->second && hwnd == it->second->_hwndButton)
it->second->onPreFileOk();
}
}
return callWindowClassProc(_T("Button"), hwnd, msg, wparam, lparam);
return ::CallNextHookEx(nullptr, nCode, wParam, lParam);
}

static LRESULT CALLBACK FileNameWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
static LRESULT CALLBACK KbdProcHook(int nCode, WPARAM wParam, LPARAM lParam)
{
auto* inst = reinterpret_cast<FileDialogEventHandler*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
if (!inst)
return callWindowClassProc(_T("Edit"), hwnd, msg, wparam, lparam);

// WM_KEYDOWN with wparam == VK_RETURN isn't delivered here.
// So watch for the keyboard input while the control has focus.
// Initially, the control has focus.
// WM_SETFOCUS is sent if control regains focus after losing it.
static bool processingReturn = false;
switch (msg)
if (nCode == HC_ACTION)
{
case WM_SETFOCUS:
inst->_monitorKeyboard = true;
break;
case WM_KILLFOCUS:
inst->_monitorKeyboard = false;
break;
}
// Avoid unnecessary processing by polling keyboard only on some messages.
bool checkMsg = msg > WM_USER;
if (inst->_monitorKeyboard && !processingReturn && checkMsg)
{
SHORT state = GetAsyncKeyState(VK_RETURN);
if (state & 0x8000)
if (wParam == VK_RETURN)
{
// Avoid re-entrance because the call might generate some messages.
processingReturn = true;
inst->onPreFileOk();
processingReturn = false;
// Handle return key passed to the file name edit box.
HWND hwnd = GetFocus();
auto it = s_handleMap.find(hwnd);
if (it != s_handleMap.end() && it->second && hwnd == it->second->_hwndNameEdit)
it->second->onPreFileOk();
}
}
return CallWindowProc(inst->_fileNameProc, hwnd, msg, wparam, lparam);
return ::CallNextHookEx(nullptr, nCode, wParam, lParam);
}

static std::unordered_map<HWND, FileDialogEventHandler*> s_handleMap;

long _cRef;
com_ptr<IFileDialog> _dialog;
com_ptr<IFileDialogCustomize> _customize;
const std::vector<Filter> _filterSpec;
generic_string _lastUsedFolder;
HHOOK _prevKbdHook = nullptr;
HHOOK _prevCallHook = nullptr;
HWND _hwndNameEdit = nullptr;
HWND _hwndButton = nullptr;
WNDPROC _okButtonProc = nullptr;
WNDPROC _fileNameProc = nullptr;
UINT _currentType = 0; // File type currenly selected in dialog.
UINT _lastSelectedType = 0; // Last selected non-wildcard file type.
UINT _wildcardType = 0; // Wildcard *.* file type index (usually 1).
bool _monitorKeyboard = true;
};
std::unordered_map<HWND, FileDialogEventHandler*> FileDialogEventHandler::s_handleMap;

///////////////////////////////////////////////////////////////////////////////

Expand Down

0 comments on commit b5a5baf

Please sign in to comment.