233 lines
10 KiB
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);
|
|
}
|