Finish up edit field accessibility.

This commit is contained in:
ak47 committed 2026-10-10 00:18:22 -04:00
1 parent 6f6d4040c4
commit c5657ee5d4
9 files changed
+361 -39

No files matched your search

+31 -19
View File
@@ -5,14 +5,13 @@
#include <stdlib.h>
#define EDIT_SUBCLASS_ID 0x57415431
#define EDIT_STATE_PROPERTY L"CthulhuWineEditState"
// Bound memory and work inside applications. Navigation reuses the snapshot;
// only text changes refresh it. Large documents retain line/character reading.
// only text changes refresh it. Large documents retain line reading.
#define MAX_SNAPSHOT_UNITS (1024 * 1024)
typedef struct EditState {
WCHAR *text;
DWORD length, start, end;
DWORD length;
BOOL busy, known, destroyed;
} EditState;
@@ -45,6 +44,7 @@ static void current_line(HWND window, DWORD caret, WCHAR *output) {
output[0] = 1023;
LRESULT copied = SendMessageW(window, EM_GETLINE, line, (LPARAM)output);
if (copied < 0 || copied > 1023) copied = 0;
if (copied && output[copied - 1] >= 0xd800 && output[copied - 1] <= 0xdbff) --copied;
output[copied] = 0;
}
@@ -85,8 +85,10 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
wp == VK_HOME || wp == VK_END || wp == VK_PRIOR || wp == VK_NEXT);
BOOL changing = message == WM_CHAR || (message == WM_KEYDOWN && wp == VK_DELETE) ||
message == WM_PASTE || message == WM_CUT || message == WM_CLEAR ||
message == WM_UNDO || message == EM_REPLACESEL || message == WM_SETTEXT;
BOOL focus = message == WM_SETFOCUS;
message == WM_UNDO || message == EM_UNDO || message == EM_REPLACESEL ||
message == WM_SETTEXT || message == EM_SETHANDLE || message == EM_FMTLINES;
BOOL requested_focus = message == RegisterWindowMessageW(EDIT_FOCUS_MESSAGE);
BOOL focus = message == WM_SETFOCUS || requested_focus;
BOOL selected = message == EM_SETSEL;
if (!navigation && !changing && !focus && !selected && message != EM_SETPASSWORDCHAR)
return DefSubclassProc(window, message, wp, lp);
@@ -97,20 +99,22 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
BOOL password = (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) != 0;
if (!password && (!state->known || focus)) snapshot(window, state);
WCHAR removed[1024] = {0};
if (!password && state->text && changing) {
if (!password && state->text && changing && old_start <= state->length && old_end <= state->length) {
DWORD first = old_start, last = old_end;
if (first == last) {
if (message == WM_CHAR && wp == VK_BACK && first) {
--first;
if (first && state->text[first] >= 0xdc00 && state->text[first] <= 0xdfff) --first;
else if (first && state->text[first] == '\n' && state->text[first - 1] == '\r') --first;
} else if (message == WM_KEYDOWN && wp == VK_DELETE && last < state->length) {
++last;
if (last < state->length && state->text[last - 1] >= 0xd800 && state->text[last - 1] <= 0xdbff) ++last;
else if (last < state->length && state->text[last - 1] == '\r' && state->text[last] == '\n') ++last;
}
}
if (last <= state->length) copy_range(removed, state->text, first, last);
}
LRESULT result = DefSubclassProc(window, message, wp, lp);
LRESULT result = requested_focus ? 0 : DefSubclassProc(window, message, wp, lp);
if (state->destroyed) {
free(state->text); free(state);
return result;
@@ -138,11 +142,11 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
// Speak the changed portion when extending or shrinking a selection.
DWORD first = start, last = end;
kind = WINE_ACCESS_TEXT_SELECTED;
if (old_start != old_end && start == old_start) {
if (old_start != old_end && old_end <= state->length && start == old_start) {
first = end < old_end ? end : old_end;
last = end > old_end ? end : old_end;
if (end < old_end) kind = WINE_ACCESS_TEXT_UNSELECTED;
} else if (old_start != old_end && end == old_end) {
} else if (old_start != old_end && old_end <= state->length && end == old_end) {
first = start < old_start ? start : old_start;
last = start > old_start ? start : old_start;
if (start > old_start) kind = WINE_ACCESS_TEXT_UNSELECTED;
@@ -151,7 +155,8 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
} else if (old_start != old_end && state->text && old_end <= state->length) {
copy_range(text, state->text, old_start, old_end);
kind = WINE_ACCESS_TEXT_UNSELECTED;
} else if (navigation && (wp == VK_UP || wp == VK_DOWN || wp == VK_PRIOR || wp == VK_NEXT)) {
} else if (navigation && (wp == VK_UP || wp == VK_DOWN || wp == VK_PRIOR || wp == VK_NEXT ||
((wp == VK_HOME || wp == VK_END) && (GetKeyState(VK_CONTROL) & 0x8000)))) {
current_line(window, start, text);
} else if (state->text && start < state->length) {
DWORD last = start + 1;
@@ -165,39 +170,46 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
current_line(window, start, text);
}
report = TRUE;
} else if (changing && removed[0] && old_length > state->length) {
} else if (changing && removed[0] && old_length > state->length &&
(message == WM_CLEAR || message == WM_CUT ||
(message == WM_CHAR && wp == VK_BACK) ||
(message == WM_KEYDOWN && wp == VK_DELETE))) {
lstrcpynW(text, removed, 1024);
kind = WINE_ACCESS_TEXT_DELETED; report = TRUE;
}
if (report) notify_text(window, kind, text, start, end);
state->start = start; state->end = end;
state->busy = FALSE;
return result;
}
void wine_access_watch_edit(HWND window) {
BOOL wine_access_watch_edit(HWND window) {
WCHAR class_name[64];
if (!window || !GetClassNameW(window, class_name, 64) || lstrcmpW(class_name, L"Edit")) return;
if (!window || !GetClassNameW(window, class_name, 64) || lstrcmpW(class_name, L"Edit")) return FALSE;
// Wine's comctl32 v5 exports GetWindowSubclass only by ordinal. Our own
// property also keeps attachment independent of the application's manifest.
if (GetPropW(window, EDIT_STATE_PROPERTY)) return;
if (GetPropW(window, EDIT_STATE_PROPERTY)) return FALSE;
EditState *state = calloc(1, sizeof(*state));
if (!state) return;
if (!state) return FALSE;
if (!SetWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID, (DWORD_PTR)state)) {
free(state); return;
free(state); return FALSE;
}
if (!SetPropW(window, EDIT_STATE_PROPERTY, state)) {
RemoveWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID);
free(state); return;
free(state); return FALSE;
}
// Subclass callbacks can outlive the reader. Keep their module mapped until
// the application exits; each control frees its state on WM_NCDESTROY.
HMODULE module;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
(LPCWSTR)(UINT_PTR)edit_proc, &module);
return TRUE;
}
__declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp, LPARAM lp) {
if (code >= 0 && wp == PM_REMOVE) wine_access_watch_edit(GetFocus());
if (code >= 0 && wp == PM_REMOVE) {
HWND focus = GetFocus();
if (wine_access_watch_edit(focus))
SendMessageW(focus, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
}
return CallNextHookEx(NULL, code, wp, lp);
}