Files
wine-access/scintilla_hook.c

233 lines
10 KiB
C

// SPDX-License-Identifier: LGPL-2.1-or-later
#include "edit_hook.h"
#include "menu_bridge.h"
#include "scintilla_messages.h"
#include <commctrl.h>
#include <stdlib.h>
#define SCINTILLA_SUBCLASS_ID 0x57415331
#define MAX_RANGE_BYTES 4096
#define SCINTILLA_CUT_MESSAGE L"CthulhuWineScintillaCut"
#define SCINTILLA_STATE_PROPERTY L"CthulhuWineScintillaState"
typedef struct ScintillaState {
BOOL busy, destroyed;
BOOL pendingCut;
ptrdiff_t cutLength;
WCHAR cutText[1024];
} ScintillaState;
static LRESULT query(HWND window, UINT message, WPARAM parameter) {
return SendMessageW(window, message, parameter, 0);
}
static void selection(HWND window, ptrdiff_t *start, ptrdiff_t *end) {
ptrdiff_t caret = query(window, SCI_GETCURRENTPOS, 0);
ptrdiff_t anchor = query(window, SCI_GETANCHOR, 0);
*start = caret < anchor ? caret : anchor;
*end = caret > anchor ? caret : anchor;
}
static BOOL simple_selection(HWND window) {
return query(window, SCI_GETSELECTIONS, 0) == 1 &&
!query(window, SCI_SELECTIONISRECTANGLE, 0);
}
static void read_range(HWND window, ptrdiff_t start, ptrdiff_t end, WCHAR *output) {
output[0] = 0;
if (start < 0 || end <= start) return;
if (end - start > MAX_RANGE_BYTES) {
// Stop on a character boundary, including UTF-8 and DBCS characters.
end = query(window, SCI_POSITIONBEFORE, start + MAX_RANGE_BYTES + 1);
}
if (end <= start || end - start > MAX_RANGE_BYTES) return;
char bytes[MAX_RANGE_BYTES + 1] = {0};
ScintillaTextRange range = {{start, end}, bytes};
LRESULT copied = SendMessageW(window, SCI_GETTEXTRANGEFULL, 0, (LPARAM)&range);
if (copied <= 0 || copied > MAX_RANGE_BYTES) return;
UINT codePage = query(window, SCI_GETCODEPAGE, 0);
if (!codePage) {
// Single-byte documents use the default style's Windows character set.
DWORD charset = query(window, SCI_STYLEGETCHARACTERSET, 32);
if (charset == DEFAULT_CHARSET) codePage = GetACP();
else {
CHARSETINFO info;
if (!TranslateCharsetInfo((DWORD *)(UINT_PTR)charset, &info, TCI_SRCCHARSET)) return;
codePage = info.ciACP;
}
}
WCHAR converted[MAX_RANGE_BYTES + 1];
int units = MultiByteToWideChar(codePage, 0, bytes, (int)copied,
converted, MAX_RANGE_BYTES);
if (units > 1023) units = 1023;
if (units && converted[units - 1] >= 0xd800 && converted[units - 1] <= 0xdbff) --units;
if (units > 0) CopyMemory(output, converted, units * sizeof(WCHAR));
output[units] = 0;
}
static void current_line(HWND window, WCHAR *output) {
ptrdiff_t caret = query(window, SCI_GETCURRENTPOS, 0);
LRESULT line = query(window, SCI_LINEFROMPOSITION, caret);
read_range(window, query(window, SCI_POSITIONFROMLINE, line),
query(window, SCI_GETLINEENDPOSITION, line), output);
}
static LRESULT CALLBACK scintilla_proc(HWND window, UINT message, WPARAM wp, LPARAM lp,
UINT_PTR id, DWORD_PTR reference) {
ScintillaState *state = (ScintillaState *)reference;
if (message == WM_NCDESTROY) {
RemoveWindowSubclass(window, scintilla_proc, id);
RemovePropW(window, EDIT_STATE_PROPERTY);
RemovePropW(window, SCINTILLA_STATE_PROPERTY);
state->destroyed = TRUE;
if (!state->busy) free(state);
return DefSubclassProc(window, message, wp, lp);
}
if (state->busy) return DefSubclassProc(window, message, wp, lp);
if (message == RegisterWindowMessageW(SCINTILLA_CUT_MESSAGE)) {
if (state->pendingCut) {
state->pendingCut = FALSE;
if (query(window, SCI_GETLENGTH, 0) < state->cutLength) {
ptrdiff_t start, end;
selection(window, &start, &end);
wine_access_notify_text(window, WINE_ACCESS_TEXT_DELETED, state->cutText,
start, end, WINE_ACCESS_TEXT_SCINTILLA);
}
}
return 0;
}
BOOL navigation = message == WM_KEYDOWN &&
(wp == VK_LEFT || wp == VK_RIGHT || wp == VK_UP || wp == VK_DOWN ||
wp == VK_HOME || wp == VK_END || wp == VK_PRIOR || wp == VK_NEXT);
BOOL requestedFocus = message == RegisterWindowMessageW(EDIT_FOCUS_MESSAGE);
BOOL focus = message == WM_SETFOCUS || requestedFocus;
BOOL selected = message == SCI_SETSEL;
BOOL control = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
BOOL backspace = message == WM_KEYDOWN && wp == VK_BACK;
BOOL deleting = backspace || (message == WM_KEYDOWN && wp == VK_DELETE) ||
(message == WM_KEYDOWN && control && wp == 'X') ||
message == WM_CUT || message == WM_CLEAR || message == SCI_CUT || message == SCI_CLEAR;
if (!navigation && !focus && !selected && !deleting)
return DefSubclassProc(window, message, wp, lp);
state->busy = TRUE;
ptrdiff_t oldStart, oldEnd;
selection(window, &oldStart, &oldEnd);
ptrdiff_t oldLength = query(window, SCI_GETLENGTH, 0);
WCHAR removed[1024] = {0};
if (deleting && !query(window, SCI_GETREADONLY, 0) && simple_selection(window)) {
ptrdiff_t first = oldStart, last = oldEnd;
// Ctrl+Backspace/Delete can remove a word; this hook only predicts plain
// character deletion. Selected ranges and cut are always explicit.
if (first == last && !control) {
if (backspace && first) first = query(window, SCI_POSITIONBEFORE, first);
else if (message == WM_KEYDOWN && wp == VK_DELETE && last < oldLength)
last = query(window, SCI_POSITIONAFTER, last);
// Treat CRLF as one line break for deletion feedback.
if (backspace && first && query(window, SCI_GETCHARAT, first) == '\n' &&
query(window, SCI_GETCHARAT, first - 1) == '\r') --first;
if (!backspace && last < oldLength && query(window, SCI_GETCHARAT, last - 1) == '\r' &&
query(window, SCI_GETCHARAT, last) == '\n') ++last;
}
read_range(window, first, last, removed);
}
// Selection feedback reads the old text before the application can change it.
WCHAR unselected[1024] = {0};
if ((navigation || selected) && oldStart != oldEnd && simple_selection(window))
read_range(window, oldStart, oldEnd, unselected);
LRESULT result = requestedFocus ? 0 : DefSubclassProc(window, message, wp, lp);
if (state->destroyed) { free(state); return result; }
ptrdiff_t start, end;
selection(window, &start, &end);
WCHAR text[1024] = {0};
UINT kind = WINE_ACCESS_TEXT_READ;
BOOL report = FALSE;
if (focus) {
current_line(window, text);
kind = WINE_ACCESS_TEXT_FOCUS;
report = TRUE;
} else if ((navigation || selected) && (start != oldStart || end != oldEnd)) {
if (start != end && simple_selection(window)) {
ptrdiff_t first = start, last = end;
kind = WINE_ACCESS_TEXT_SELECTED;
if (oldStart != oldEnd && start == oldStart) {
first = end < oldEnd ? end : oldEnd;
last = end > oldEnd ? end : oldEnd;
if (end < oldEnd) kind = WINE_ACCESS_TEXT_UNSELECTED;
} else if (oldStart != oldEnd && end == oldEnd) {
first = start < oldStart ? start : oldStart;
last = start > oldStart ? start : oldStart;
if (start > oldStart) kind = WINE_ACCESS_TEXT_UNSELECTED;
}
read_range(window, first, last, text);
} else if (unselected[0]) {
lstrcpynW(text, unselected, 1024);
kind = WINE_ACCESS_TEXT_UNSELECTED;
} else if (navigation && (wp == VK_UP || wp == VK_DOWN || wp == VK_PRIOR || wp == VK_NEXT ||
((wp == VK_HOME || wp == VK_END) && control))) {
current_line(window, text);
} else {
ptrdiff_t caret = query(window, SCI_GETCURRENTPOS, 0);
ptrdiff_t last = query(window, SCI_POSITIONAFTER, caret);
if (control && navigation) {
last = SendMessageW(window, SCI_WORDENDPOSITION, caret, FALSE);
// Windows word navigation includes trailing spaces.
ptrdiff_t limit = query(window, SCI_GETLENGTH, 0);
while (last < limit && last - caret < MAX_RANGE_BYTES &&
query(window, SCI_GETCHARAT, last) == ' ') ++last;
}
read_range(window, caret, last, text);
}
report = TRUE;
} else if (deleting && removed[0] && query(window, SCI_GETLENGTH, 0) < oldLength) {
lstrcpynW(text, removed, 1024);
kind = backspace ? WINE_ACCESS_TEXT_BACKSPACED : WINE_ACCESS_TEXT_DELETED;
report = TRUE;
}
if (report) {
UINT flags = WINE_ACCESS_TEXT_SCINTILLA;
if (query(window, SCI_GETREADONLY, 0)) flags |= WINE_ACCESS_TEXT_READONLY;
wine_access_notify_text(window, kind, text, start, end, flags);
if (deleting) state->pendingCut = FALSE;
}
state->busy = FALSE;
return result;
}
BOOL wine_access_watch_scintilla(HWND window) {
if (GetPropW(window, EDIT_STATE_PROPERTY)) return FALSE;
ScintillaState *state = calloc(1, sizeof(*state));
if (!state) return FALSE;
if (!SetWindowSubclass(window, scintilla_proc, SCINTILLA_SUBCLASS_ID, (DWORD_PTR)state)) {
free(state); return FALSE;
}
if (!SetPropW(window, EDIT_STATE_PROPERTY, state) ||
!SetPropW(window, SCINTILLA_STATE_PROPERTY, state)) {
RemovePropW(window, EDIT_STATE_PROPERTY);
RemoveWindowSubclass(window, scintilla_proc, SCINTILLA_SUBCLASS_ID);
free(state); return FALSE;
}
HMODULE module;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
(LPCWSTR)(UINT_PTR)scintilla_proc, &module);
return TRUE;
}
void wine_access_scintilla_input(HWND window, const MSG *message) {
if (message->message != WM_KEYDOWN || message->wParam != 'X' ||
!(GetKeyState(VK_CONTROL) & 0x8000)) return;
ScintillaState *state = (ScintillaState *)GetPropW(window, SCINTILLA_STATE_PROPERTY);
if (!state || state->busy || query(window, SCI_GETREADONLY, 0) || !simple_selection(window)) return;
ptrdiff_t start, end;
selection(window, &start, &end);
if (start == end) return;
// Notepad++ translates Ctrl+X before dispatch and calls Scintilla's direct
// function, bypassing the subclass. Capture only the selected bounded range
// before translation, then check the result on the application's queue.
state->cutLength = query(window, SCI_GETLENGTH, 0);
read_range(window, start, end, state->cutText);
state->pendingCut = state->cutText[0] &&
PostMessageW(window, RegisterWindowMessageW(SCINTILLA_CUT_MESSAGE), 0, 0);
}