#include "../resources/resource.h" #include "app.h" #include "history_preview.h" #include "ui_scroll.h" #include "configuration.h" #include "ocr.h" #include "richtext.h" #include "ui_geometry.h" #include "diagnostics.h" #include #include #include #include #include #include #include #include namespace ks { namespace { constexpr UINT ResultsMessage = WM_APP + 40, PreviewMessage = WM_APP + 41; int scale(HWND w, int n) { return MulDiv(n, (int)GetDpiForWindow(w), 96); } HFONT font(HWND w) { return CreateFontW(-scale(w, 14), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, L"Segoe UI"); } struct WindowLayout { int width{}, height{}, x{}, y{}; bool scrolling{}, arranging{}, history{}; int wheelRemainder{}, horizontalWheelRemainder{}; std::vector> controls; // Original 96-DPI geometry; never rescale rounded pixels. HWND stretch{}, bottomLeft{}, bottomRight{}, details{}; bool autoOptions{}; }; WindowLayout *windowLayoutState(HWND w) { return reinterpret_cast(GetPropW(w, L"WindowLayout")); } RECT workArea(RECT rect) { MONITORINFO monitor{sizeof(monitor)}; GetMonitorInfoW(MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST), &monitor); return monitor.rcWork; } void fitToMonitor(HWND w, RECT rect) { rect = fitWindowRect(rect, workArea(rect)); SetWindowPos(w, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE | SWP_NOZORDER); } void arrangeWindow(HWND w) { auto *layout = windowLayoutState(w); if (!layout || !layout->scrolling || layout->arranging) return; layout->arranging = true; // Showing either scrollbar can make the other necessary. Let the client size settle. for (int pass = 0; pass < 3; ++pass) { RECT client{}; GetClientRect(w, &client); SCROLLINFO horizontal{sizeof(horizontal), SIF_RANGE | SIF_PAGE | SIF_POS, 0, scale(w, layout->width) - 1, (UINT)client.right, layout->x}; SCROLLINFO vertical{sizeof(vertical), SIF_RANGE | SIF_PAGE | SIF_POS, 0, scale(w, layout->height) - 1, (UINT)client.bottom, layout->y}; layout->x = SetScrollInfo(w, SB_HORZ, &horizontal, TRUE); layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE); } RECT client{}; GetClientRect(w, &client); bool expandable = layout->history || layout->stretch || layout->autoOptions; int extraWidth = expandable ? std::max(0, int(client.right) - scale(w, layout->width)) : 0; int extraHeight = expandable ? std::max(0, int(client.bottom) - scale(w, layout->height)) : 0; struct Move { HWND child; RECT rect; }; std::vector moves; for (const auto &[child, r] : layout->controls) { int id = GetDlgCtrlID(child); RECT next{scale(w, r.left) - layout->x, scale(w, r.top) - layout->y + ((id >= 110 && id <= 114) || id == 116 ? extraHeight : 0)}; next.right = next.left + scale(w, r.right - r.left) + (id == 101 || id == 102 ? extraWidth : 0); next.bottom = next.top + scale(w, r.bottom - r.top) + (id == 102 ? extraHeight : 0); if (child == layout->stretch) { next.right += extraWidth; next.bottom += extraHeight; } if (child == layout->details) { next.right += extraWidth; OffsetRect(&next, 0, extraHeight); } if (child == layout->bottomLeft || child == layout->bottomRight) OffsetRect(&next, child == layout->bottomRight ? extraWidth : 0, extraHeight); if (layout->autoOptions) { if (r.top >= 396) OffsetRect(&next, 0, extraHeight); if (r.top == 302 || r.top == 328 || (id >= 401 && id <= 403)) { int column = id >= 401 && id <= 403 ? id - 401 : (r.left - 18) / 178; int leftExtra = extraWidth * column / 3; OffsetRect(&next, leftExtra, 0); next.right += extraWidth * (column + 1) / 3 - leftExtra; if (r.top == 328) next.bottom += extraHeight; } else if (r.top == 474 || r.top == 500) { bool rightColumn = r.left >= 290; OffsetRect(&next, rightColumn ? extraWidth / 2 : 0, 0); next.right += rightColumn ? extraWidth - extraWidth / 2 : extraWidth / 2; } else if (id == 406 || id == 410 || id == IDOK || id == IDCANCEL) OffsetRect(&next, extraWidth, 0); else if (r.right - r.left >= 500) next.right += extraWidth; } RECT current{}; GetWindowRect(child, ¤t); MapWindowPoints(nullptr, w, reinterpret_cast(¤t), 2); if (!EqualRect(¤t, &next)) moves.push_back({child, next}); } if (!moves.empty()) { auto batch = BeginDeferWindowPos((int)moves.size()); for (const auto &move : moves) { if (!batch) break; auto r = move.rect; batch = DeferWindowPos(batch, move.child, nullptr, r.left, r.top, r.right-r.left, r.bottom-r.top, SWP_NOACTIVATE | SWP_NOZORDER); } if (!batch || !EndDeferWindowPos(batch)) for (const auto &move : moves) { auto r = move.rect; SetWindowPos(move.child, nullptr, r.left, r.top, r.right-r.left, r.bottom-r.top, SWP_NOACTIVATE | SWP_NOZORDER); } } layout->arranging = false; } void scrollWindow(HWND w, WindowLayout *layout, bool horizontal, int requested) { SCROLLINFO info{sizeof(info), SIF_RANGE | SIF_PAGE}; GetScrollInfo(w, horizontal ? SB_HORZ : SB_VERT, &info); int next = std::clamp(requested, info.nMin, std::max(info.nMin, info.nMax - std::max(0, (int)info.nPage - 1))); int &position = horizontal ? layout->x : layout->y; if (position == next) return; position = next; arrangeWindow(w); } void revealControl(HWND child) { HWND parent = GetParent(child); auto *layout = windowLayoutState(parent); if (!layout || !layout->scrolling) return; RECT r{}, client{}; GetWindowRect(child, &r); MapWindowPoints(nullptr, parent, reinterpret_cast(&r), 2); GetClientRect(parent, &client); if (r.left < 0) layout->x += r.left; else if (r.right > client.right) layout->x += r.right - client.right; if (r.top < 0) layout->y += r.top; else if (r.bottom > client.bottom) layout->y += r.bottom - client.bottom; arrangeWindow(parent); } LRESULT CALLBACK scrollChildProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR) { if (msg == WM_SETFOCUS) revealControl(w); if (msg == WM_NCDESTROY) RemoveWindowSubclass(w, scrollChildProc, 2); return DefSubclassProc(w, msg, wp, lp); } void resizeForDpi(HWND, WPARAM, LPARAM); LRESULT CALLBACK layoutProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR) { auto *layout = windowLayoutState(w); if (!layout) return DefSubclassProc(w, msg, wp, lp); if (msg == WM_DPICHANGED) { resizeForDpi(w, wp, lp); return 0; } if (msg == WM_SIZE) arrangeWindow(w); if (msg == WM_DISPLAYCHANGE || msg == WM_SETTINGCHANGE || (msg == WM_SHOWWINDOW && wp)) { RECT rect{}; GetWindowRect(w, &rect); fitToMonitor(w, rect); arrangeWindow(w); } if (layout->scrolling && (msg == WM_VSCROLL || msg == WM_HSCROLL)) { bool vertical = msg == WM_VSCROLL; SCROLLINFO info{sizeof(info), SIF_ALL}; GetScrollInfo(w, vertical ? SB_VERT : SB_HORZ, &info); int position = vertical ? layout->y : layout->x; switch (LOWORD(wp)) { case SB_LINEUP: position -= scale(w, 32); break; case SB_LINEDOWN: position += scale(w, 32); break; case SB_PAGEUP: position -= (int)info.nPage; break; case SB_PAGEDOWN: position += (int)info.nPage; break; case SB_TOP: position = 0; break; case SB_BOTTOM: position = info.nMax; break; case SB_THUMBTRACK: case SB_THUMBPOSITION: position = info.nTrackPos; break; } scrollWindow(w, layout, !vertical, position); return 0; } if (layout->scrolling && (msg == WM_MOUSEWHEEL || msg == WM_MOUSEHWHEEL)) { bool horizontal = msg == WM_MOUSEHWHEEL || (GET_KEYSTATE_WPARAM(wp) & MK_SHIFT); int &remainder = horizontal ? layout->horizontalWheelRemainder : layout->wheelRemainder; remainder += GET_WHEEL_DELTA_WPARAM(wp); int steps = remainder / WHEEL_DELTA; remainder %= WHEEL_DELTA; if (steps) scrollWindow(w, layout, horizontal, (horizontal ? layout->x : layout->y) + steps * scale(w, 48) * (msg == WM_MOUSEHWHEEL ? 1 : -1)); return 0; } if (msg == WM_NCDESTROY) { RemovePropW(w, L"WindowLayout"); RemoveWindowSubclass(w, layoutProc, 2); delete layout; } return DefSubclassProc(w, msg, wp, lp); } HWND control(HWND parent, const wchar_t *cls, const wchar_t *title, DWORD style, int id, int x, int y, int width, int height) { HWND w = CreateWindowExW(wcscmp(cls, L"EDIT") == 0 ? WS_EX_CLIENTEDGE : 0, cls, title, WS_CHILD | WS_VISIBLE | style, scale(parent, x), scale(parent, y), scale(parent, width), scale(parent, height), parent, (HMENU)(INT_PTR)id, GetModuleHandleW(nullptr), nullptr); SendMessageW(w, WM_SETFONT, (WPARAM)GetPropW(parent, L"Font"), TRUE); if(wcscmp(cls,L"LISTBOX")==0 || wcscmp(cls,WC_LISTVIEWW)==0 || wcscmp(cls,L"COMBOBOX")==0 || (wcscmp(cls,L"EDIT")==0 && (style&ES_MULTILINE))) enableWheelBatching(w,wcscmp(cls,L"LISTBOX")==0); if (auto *layout = windowLayoutState(parent); layout && layout->scrolling) { layout->controls.push_back({w, {x, y, x + width, y + height}}); SetWindowSubclass(w, scrollChildProc, 2, 0); arrangeWindow(parent); } return w; } void label(HWND w, const wchar_t *text, int x, int y, int width = 200) { control(w, L"STATIC", text, 0, 0, x, y, width, 22); } void button(HWND w, const wchar_t *text, int id, int x, int y, int width = 100) { control(w, L"BUTTON", text, WS_TABSTOP | BS_PUSHBUTTON, id, x, y, width, 28); } HWND edit(HWND w, int id, int x, int y, int width, int height = 25, DWORD style = ES_AUTOHSCROLL) { auto c = control(w, L"EDIT", L"", WS_TABSTOP | style, id, x, y, width, height); SendMessageW(c, EM_SETLIMITTEXT, 1024 * 1024, 0); return c; } LRESULT CALLBACK hotkeyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR data) { auto *app = (App *)data; if (msg == WM_SETFOCUS) { app->setHotkeyCapture(w); SendMessageW(w, EM_SETSEL, 0, -1); } if (msg == WM_KILLFOCUS || msg == WM_NCDESTROY) { if (app->hotkeyCapture == w) app->setHotkeyCapture(nullptr); } if (msg == CaptureHotkeyMessage) { UINT mods = (UINT)lp; if ((wp == VK_TAB && (mods == 0 || mods == MOD_SHIFT)) || (wp == VK_ESCAPE && mods == 0)) { SetFocus(GetNextDlgTabItem(GetParent(w), w, mods == MOD_SHIFT)); return 0; } if ((wp == VK_BACK || wp == VK_DELETE) && mods == 0) { SetWindowTextW(w, L""); return 0; } auto text = Hotkey{mods, (UINT)wp}.format(); if (!text.empty()) { SetWindowTextW(w, text.c_str()); SendMessageW(w, EM_SETSEL, 0, -1); } return 0; } if (msg == WM_CHAR || msg == WM_SYSCHAR || msg == WM_PASTE || msg == WM_CUT) return 0; return DefSubclassProc(w, msg, wp, lp); } [[maybe_unused]] void hotkeyEdit(App &app, HWND w, int id, int x, int y, int width) { HWND field = edit(w, id, x, y, width, 25, ES_AUTOHSCROLL | ES_READONLY); SendMessageW(field, EM_SETCUEBANNER, FALSE, (LPARAM)L"Click, then press a shortcut"); SetWindowSubclass(field, hotkeyProc, 1, (DWORD_PTR)&app); } [[maybe_unused]] void checkbox(HWND w, const wchar_t *text, int id, int x, int y, int width = 300) { control(w, L"BUTTON", text, WS_TABSTOP | BS_AUTOCHECKBOX, id, x, y, width, 24); } std::wstring value(HWND w, int id) { HWND c = GetDlgItem(w, id); int n = GetWindowTextLengthW(c); std::wstring text(n + 1, L'\0'); GetWindowTextW(c, text.data(), n + 1); text.resize(n); return text; } [[maybe_unused]] void value(HWND w, int id, const std::wstring &text) { SetDlgItemTextW(w, id, text.c_str()); } [[maybe_unused]] bool checked(HWND w, int id) { return IsDlgButtonChecked(w, id) == BST_CHECKED; } [[maybe_unused]] void check(HWND w, int id, bool yes) { CheckDlgButton(w, id, yes ? BST_CHECKED : BST_UNCHECKED); } void error(HWND w, const std::exception &e) { MessageBoxW(w, wide(e.what()).c_str(), L"KSwitcher", MB_OK | MB_ICONWARNING); } HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc, int width, int height, DWORD extra = 0, bool resizable = true) { WNDCLASSW wc{}; wc.hInstance = app.instance; wc.lpfnWndProc = proc; wc.lpszClassName = cls; wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1); wc.hIcon = LoadIconW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER)); RegisterClassW(&wc); UINT dpi = GetDpiForSystem(); RECT rect{0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)}; DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_CLIPCHILDREN | extra; if (resizable) style |= WS_THICKFRAME | WS_MAXIMIZEBOX; AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi); POINT cursor{}; GetCursorPos(&cursor); MONITORINFO monitor{sizeof(monitor)}; GetMonitorInfoW(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &monitor); HWND w = CreateWindowExW(WS_EX_APPWINDOW, cls, title, style, monitor.rcWork.left, monitor.rcWork.top, rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, app.instance, &app); if (!w) throw std::runtime_error(utf8(lastError(L"Cannot create native window"))); // The system DPI may still describe the old primary display. The HWND tells // us the actual destination DPI, and must size the frame AND its controls. dpi = GetDpiForWindow(w); rect = {0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)}; AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi); int outerWidth = rect.right - rect.left, outerHeight = rect.bottom - rect.top; int left = monitor.rcWork.left + std::max(0L, (monitor.rcWork.right - monitor.rcWork.left - outerWidth) / 2); int top = monitor.rcWork.top + std::max(0L, (monitor.rcWork.bottom - monitor.rcWork.top - outerHeight) / 2); SetPropW(w, L"WindowLayout", new WindowLayout{width, height, 0, 0, !(extra & WS_THICKFRAME)}); SetWindowSubclass(w, layoutProc, 2, 0); if(windowLayoutState(w)->scrolling) enableWheelBatching(w); RECT initial = fitWindowRect({left, top, left + outerWidth, top + outerHeight}, monitor.rcWork); SetWindowPos(w, nullptr, initial.left, initial.top, initial.right - initial.left, initial.bottom - initial.top, SWP_NOACTIVATE | SWP_NOZORDER); arrangeWindow(w); SetWindowTextW(w, localBuildTitle(title).c_str()); auto smallIcon = (HICON)LoadImageW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON, GetSystemMetricsForDpi(SM_CXSMICON, dpi), GetSystemMetricsForDpi(SM_CYSMICON, dpi), LR_SHARED); SendMessageW(w, WM_SETICON, ICON_SMALL, (LPARAM)smallIcon); SetPropW(w, L"Font", font(w)); SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)GetDpiForWindow(w)); return w; } void resizeForDpi(HWND w, WPARAM wp, LPARAM lp) { // Creation may send WM_DPICHANGED before controls and the original font exist. if (!GetPropW(w, L"Dpi")) { fitToMonitor(w, *(RECT *)lp); return; } int previous = (int)(UINT_PTR)GetPropW(w, L"Dpi"); int next = LOWORD(wp); if (!previous) previous = 96; HFONT old = (HFONT)GetPropW(w, L"Font"); HFONT replacement = font(w); struct State { HWND parent; int previous, next; HFONT font; } state{w, previous, next, replacement}; auto *layout = windowLayoutState(w); if (!layout || !layout->scrolling) EnumChildWindows( w, [](HWND child, LPARAM data) -> BOOL { auto &s = *(State *)data; if (GetParent(child) != s.parent) return TRUE; RECT r; GetWindowRect(child, &r); MapWindowPoints(nullptr, s.parent, (POINT *)&r, 2); SetWindowPos(child, nullptr, MulDiv(r.left, s.next, s.previous), MulDiv(r.top, s.next, s.previous), MulDiv(r.right - r.left, s.next, s.previous), MulDiv(r.bottom - r.top, s.next, s.previous), SWP_NOZORDER); SendMessageW(child, WM_SETFONT, (WPARAM)s.font, TRUE); return TRUE; }, (LPARAM)&state); else { layout->x = MulDiv(layout->x, next, previous); layout->y = MulDiv(layout->y, next, previous); for (auto &[child, rect] : layout->controls) SendMessageW(child, WM_SETFONT, (WPARAM)replacement, TRUE); } SetPropW(w, L"Font", replacement); SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)next); if (old) DeleteObject(old); // layoutProc consumes WM_DPICHANGED, so owner-drawn rows must be resized here. if (layout && layout->history) SendDlgItemMessageW(w, 102, LB_SETITEMHEIGHT, 0, scale(w, 88)); fitToMonitor(w, *(RECT *)lp); arrangeWindow(w); } [[maybe_unused]] std::filesystem::path pickFile(HWND owner, bool save) { wchar_t path[32768]{}; OPENFILENAMEW ofn{sizeof(ofn)}; ofn.hwndOwner = owner; ofn.lpstrFilter = L"Settings (*.json)\0*.json\0All files\0*.*\0"; ofn.lpstrFile = path; ofn.nMaxFile = 32768; ofn.lpstrDefExt = L"json"; ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR | (save ? OFN_OVERWRITEPROMPT : OFN_FILEMUSTEXIST); return (save ? GetSaveFileNameW(&ofn) : GetOpenFileNameW(&ofn)) ? std::filesystem::path(path) : std::filesystem::path(); } void disposeFont(HWND w) { auto h = (HFONT)RemovePropW(w, L"Font"); if (h) DeleteObject(h); } #ifndef KS_CONFIG_APP struct SearchResult { uint64_t generation; std::vector items; }; struct PreviewResult { uint64_t generation; std::wstring id; Pixels pixels; std::string rtf; }; struct HistoryView { App *app{}; std::vector items; uint64_t generation{}; std::map thumbnails; std::map richTexts; HWND rich{}; std::set pending; bool scrolling{}; }; bool selectedImage(HWND w, HistoryView *v) { int i=(int)SendDlgItemMessageW(w,102,LB_GETCURSEL,0,0); return i>=0 && i<(int)v->items.size() && v->items[i].image; } void updateHistoryActions(HWND w, HistoryView *v) { SetDlgItemTextW(w,114,selectedImage(w,v)?L"&Edit image":L"&Open item"); } std::wstring selected(HWND w, HistoryView *v) { int i = (int)SendDlgItemMessageW(w, 102, LB_GETCURSEL, 0, 0); return i >= 0 && i < (int)v->items.size() ? v->items[i].id : L""; } void search(HWND w, HistoryView *v) { auto q = value(w, 101); auto generation = ++v->generation; v->pending.clear(); auto *app = v->app; app->enqueueHistorySearch( [w, app, q, generation] { try { auto result = new SearchResult{generation, app->history->search(q)}; if (!PostMessageW(w, ResultsMessage, 0, (LPARAM)result)) delete result; } catch (const std::exception &e) { app->report(wide(e.what())); } }); } void requestPreview(HWND w, HistoryView *v, const Entry &entry) { if(v->scrolling) return; const auto id = entry.id; const bool image = entry.image; if (v->thumbnails.contains(id) || v->richTexts.contains(id) || !v->pending.insert(id).second) return; auto generation = v->generation; auto *app = v->app; app->enqueue( [w, app, id, generation, image] { auto result = new PreviewResult{generation, id, {}, {}}; try { auto clip = app->history->load(id); if (image) result->pixels = decodeImage(clip, 208, 128); else result->rtf = richPreview(clip); } catch (const std::exception &) { } if (!PostMessageW(w, PreviewMessage, 0, (LPARAM)result)) delete result; }); } void drawHistoryRow(HWND w, HistoryView *v, const DRAWITEMSTRUCT &draw) { if (draw.itemID >= v->items.size()) return; const auto &entry = v->items[draw.itemID]; const bool selected = (draw.itemState & ODS_SELECTED) != 0; HDC dc = draw.hDC; int saved = SaveDC(dc); auto box = draw.rcItem; FillRect(dc, &box, GetSysColorBrush(selected ? COLOR_HIGHLIGHT : COLOR_WINDOW)); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, GetSysColor(selected ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT)); SelectObject(dc, GetPropW(w, L"Font")); RECT icon{box.left + scale(w, 12), box.top + scale(w, 10), box.left + scale(w, 116), box.bottom - scale(w, 10)}; auto thumbnail = v->thumbnails.find(entry.id); if (entry.image && thumbnail != v->thumbnails.end() && !thumbnail->second.bgra.empty()) { auto &p = thumbnail->second; double ratio = std::min(double(icon.right - icon.left) / p.width, double(icon.bottom - icon.top) / p.height); int width = int(p.width * ratio), height = int(p.height * ratio); BITMAPINFO info{}; info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); info.bmiHeader.biWidth = p.width; info.bmiHeader.biHeight = -(LONG)p.height; info.bmiHeader.biPlanes = 1; info.bmiHeader.biBitCount = 32; SetStretchBltMode(dc, HALFTONE); StretchDIBits(dc, icon.left + (icon.right - icon.left - width) / 2, icon.top + (icon.bottom - icon.top - height) / 2, width, height, 0, 0, p.width, p.height, p.bgra.data(), &info, DIB_RGB_COLORS, SRCCOPY); } else { FrameRect(dc, &icon, GetSysColorBrush(selected ? COLOR_HIGHLIGHTTEXT : COLOR_3DSHADOW)); const wchar_t *glyph = entry.image ? L"▧" : entry.files ? L"≡" : L"Aa"; DrawTextW(dc, glyph, -1, &icon, DT_CENTER | DT_VCENTER | DT_SINGLELINE); if (entry.image) requestPreview(w, v, entry); } RECT title{icon.right + scale(w, 14), box.top + scale(w, 10), box.right - scale(w, 14), box.bottom - scale(w, 30)}; auto text = (entry.pinned ? L"★ " : L"") + (entry.image ? L"Image" : entry.files ? L"Files" : entry.preview); bool formatted = false; if (!entry.image && !entry.files) { auto rich = v->richTexts.find(entry.id); if (rich != v->richTexts.end() && v->rich && !rich->second.empty()) { setRichText(v->rich, rich->second); CHARFORMAT2W format{}; format.cbSize = sizeof(format); format.dwMask = CFM_COLOR; format.crTextColor = GetSysColor(selected ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT); SendMessageW(v->rich, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&format); SendMessageW(v->rich, EM_SETBKGNDCOLOR, 0, GetSysColor(selected ? COLOR_HIGHLIGHT : COLOR_WINDOW)); FORMATRANGE range{}; range.hdc = range.hdcTarget = dc; range.rc = {MulDiv(title.left, 1440, GetDeviceCaps(dc, LOGPIXELSX)), MulDiv(title.top, 1440, GetDeviceCaps(dc, LOGPIXELSY)), MulDiv(title.right, 1440, GetDeviceCaps(dc, LOGPIXELSX)), MulDiv(title.bottom, 1440, GetDeviceCaps(dc, LOGPIXELSY))}; range.rcPage = range.rc; range.chrg = {0, -1}; IntersectClipRect(dc, title.left, title.top, title.right, title.bottom); SendMessageW(v->rich, EM_FORMATRANGE, TRUE, (LPARAM)&range); SendMessageW(v->rich, EM_FORMATRANGE, FALSE, 0); // Restore the row clipping before drawing its source label. RestoreDC(dc, saved); saved = SaveDC(dc); SetBkMode(dc, TRANSPARENT); SelectObject(dc, GetPropW(w, L"Font")); formatted = true; } else requestPreview(w, v, entry); } if (!formatted) DrawTextW(dc, text.c_str(), -1, &title, DT_WORDBREAK | DT_END_ELLIPSIS | DT_NOPREFIX); RECT detail{title.left, box.bottom - scale(w, 26), title.right, box.bottom - scale(w, 8)}; SetTextColor(dc, GetSysColor(selected ? COLOR_HIGHLIGHTTEXT : COLOR_GRAYTEXT)); auto subtitle = (entry.pinned ? L"★ " : L"") + std::to_wstring(draw.itemID + 1) + L" · " + entry.app; DrawTextW(dc, subtitle.c_str(), -1, &detail, DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX); if (draw.itemState & ODS_FOCUS) { InflateRect(&box, -2, -2); DrawFocusRect(dc, &box); } RestoreDC(dc, saved); } LRESULT CALLBACK historyKey(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR data) { HWND parent = (HWND)data; if (msg == WM_KEYDOWN) { if (wp == VK_ESCAPE) { SendMessageW(parent, WM_CLOSE, 0, 0); return 0; } if (wp == VK_RETURN) { SendMessageW(parent, WM_COMMAND, (GetKeyState(VK_SHIFT) & 0x8000) ? 111 : 110, 0); return 0; } if (GetKeyState(VK_CONTROL) & 0x8000) { if (wp >= L'1' && wp <= L'9') { SendDlgItemMessageW(parent, 102, LB_SETCURSEL, wp - L'1', 0); SendMessageW(parent, WM_COMMAND, 110, 0); return 0; } if (wp == L'P') { SendMessageW(parent, WM_COMMAND, 112, 0); return 0; } if (wp == L'O') { SendMessageW(parent, WM_COMMAND, 114, 0); return 0; } } if (GetDlgCtrlID(w) == 102 && wp == VK_DELETE) { SendMessageW(parent, WM_COMMAND, 113, 0); return 0; } if (GetDlgCtrlID(w) == 101 && wp == VK_DOWN) { SetFocus(GetDlgItem(parent, 102)); return 0; } } return DefSubclassProc(w, msg, wp, lp); } LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { auto *v = (HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA); if (msg == WM_NCCREATE) { auto *app = (App *)((CREATESTRUCTW *)lp)->lpCreateParams; v = new HistoryView; v->app = app; SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v); return TRUE; } if (!v) return DefWindowProcW(w, msg, wp, lp); try { switch (msg) { case ScrollActivity: if((HWND)wp==GetDlgItem(w,102)) { v->scrolling=lp!=0; if(!v->scrolling) InvalidateRect((HWND)wp,nullptr,FALSE); } return 0; case WM_COMMAND: { int id = LOWORD(wp); if (id == 101 && HIWORD(wp) == EN_CHANGE) { // Reject an older result even during the debounce interval. ++v->generation; SetTimer(w, 1, 180, nullptr); return 0; } if (id == 102 && HIWORD(wp) == LBN_SELCHANGE) { updateHistoryActions(w,v); return 0; } if (id == 102 && HIWORD(wp) == LBN_DBLCLK) id = 110; if (id == 116) { if (MessageBoxW(w, L"Delete all clipboard history, including pinned items? This also deletes items hidden by the current search.", L"Clear history", MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2) == IDYES) v->app->clearHistory(); return 0; } auto entry = selected(w, v); if (id == 110 || id == 111) { if (!entry.empty()) v->app->pasteEntry(entry, id == 111); return 0; } if (id == 112 || id == 113) { if (!entry.empty()) { if (id == 112) v->app->history->pin(entry); else v->app->history->remove(entry); search(w, v); } return 0; } if (id == 115 && !entry.empty()) { v->app->recognizeEntry(entry); return 0; } if (id == 114 && !entry.empty()) { showEntry(*v->app, entry); return 0; } return 0; } case WM_TIMER: KillTimer(w, 1); search(w, v); return 0; case ResultsMessage: { std::unique_ptr result((SearchResult *)lp); if (result->generation != v->generation) return 0; auto old = selected(w, v); int top=(int)SendDlgItemMessageW(w,102,LB_GETTOPINDEX,0,0); auto topId=top>=0 && top<(int)v->items.size()?v->items[top].id:L""; v->items = std::move(result->items); SendDlgItemMessageW(w, 102, WM_SETREDRAW, FALSE, 0); SendDlgItemMessageW(w, 102, LB_RESETCONTENT, 0, 0); int select = 0; int restoreTop=0; for (size_t i = 0; i < v->items.size(); ++i) { auto &e = v->items[i]; auto text = (e.pinned ? L"★ " : L"") + e.preview + L" — " + e.app; SendDlgItemMessageW(w, 102, LB_ADDSTRING, 0, (LPARAM)text.c_str()); if (e.id == old) select = (int)i; if(e.id==topId) restoreTop=(int)i; } SendDlgItemMessageW(w, 102, LB_SETCURSEL, select, 0); SendDlgItemMessageW(w,102,LB_SETTOPINDEX,restoreTop,0); updateHistoryActions(w,v); SendDlgItemMessageW(w, 102, WM_SETREDRAW, TRUE, 0); InvalidateRect(GetDlgItem(w, 102), nullptr, TRUE); return 0; } case PreviewMessage: { std::unique_ptr result((PreviewResult *)lp); if (result->generation != v->generation || !IsWindowVisible(w)) return 0; v->pending.erase(result->id); auto list=GetDlgItem(w,102); RECT client{}; GetClientRect(list,&client); std::set visible; RECT changed{}; bool repaint=false; int top=(int)SendMessageW(list,LB_GETTOPINDEX,0,0); for(int i=std::max(0,top);i<(int)v->items.size();++i) { RECT row{}, intersection{}; if(SendMessageW(list,LB_GETITEMRECT,i,(LPARAM)&row)==LB_ERR || row.top>=client.bottom) break; if(!IntersectRect(&intersection,&row,&client)) continue; visible.insert(v->items[i].id); if(v->items[i].id==result->id) { changed=intersection; repaint=true; } } if (!result->rtf.empty()) v->richTexts[result->id] = std::move(result->rtf); else v->thumbnails[result->id] = std::move(result->pixels); trimHistoryPreviews(v->thumbnails,visible); trimHistoryPreviews(v->richTexts,visible); if(repaint) InvalidateRect(list,&changed,FALSE); return 0; } case WM_MEASUREITEM: if (((MEASUREITEMSTRUCT *)lp)->CtlID == 102) { ((MEASUREITEMSTRUCT *)lp)->itemHeight = scale(w, 88); return TRUE; } break; case WM_DRAWITEM: if (((DRAWITEMSTRUCT *)lp)->CtlID == 102) { drawHistoryRow(w, v, *(DRAWITEMSTRUCT *)lp); return TRUE; } break; case WM_CONTEXTMENU: { auto list=GetDlgItem(w,102); if((HWND)wp!=list) break; POINT p{(short)LOWORD(lp),(short)HIWORD(lp)}; if(lp!=-1) { POINT hit=p; ScreenToClient(list,&hit); auto item=SendMessageW(list,LB_ITEMFROMPOINT,0,MAKELPARAM(hit.x,hit.y)); if(HIWORD(item)) return 0; SendMessageW(list,LB_SETCURSEL,LOWORD(item),0); updateHistoryActions(w,v); } else { RECT row{}; auto index=SendMessageW(list,LB_GETCURSEL,0,0); if(index==LB_ERR) return 0; SendMessageW(list,LB_GETITEMRECT,index,(LPARAM)&row); p={row.left,row.top}; ClientToScreen(list,&p); } HMENU menu = CreatePopupMenu(); AppendMenuW(menu, MF_STRING, 110, L"&Paste"); AppendMenuW(menu, MF_STRING, 111, L"Paste plain &text"); AppendMenuW(menu, MF_STRING, 112, L"P&in / unpin"); AppendMenuW(menu, MF_STRING, 113, L"&Delete"); AppendMenuW(menu, MF_STRING, 114, selectedImage(w,v)?L"&Edit image in screenshot editor":L"&Open full item"); AppendMenuW(menu, MF_STRING, 115, L"&Copy text from image (OCR)"); showMenuAccessKeys(menu); int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, p.x, p.y, 0, w, nullptr); DestroyMenu(menu); if (cmd) SendMessageW(w, WM_COMMAND, cmd, 0); return 0; } case WM_CLOSE: ShowWindow(w, SW_HIDE); v->thumbnails.clear(); v->richTexts.clear(); if (v->rich) SetWindowTextW(v->rich, L""); v->pending.clear(); ++v->generation; return 0; case WM_NCDESTROY: disposeFont(w); v->app->historyWindow = nullptr; delete v; return DefWindowProcW(w, msg, wp, lp); } } catch (const std::exception &e) { error(w, e); } return DefWindowProcW(w, msg, wp, lp); } #endif #ifndef KS_RESIDENT_APP struct SettingsView { App *app{}; Settings draft; std::vector order; int selection = -1; bool filling{}; }; void collectLayout(HWND w, SettingsView *v) { if (v->selection < 0 || v->selection >= (int)v->order.size()) return; int index = v->order[v->selection]; auto id = v->app->layouts[index].id; auto &cycle = v->draft.cycle; std::erase(cycle, id); if (checked(w, 230)) cycle.push_back(id); if (v->draft.direct.size() < v->app->layouts.size()) v->draft.direct.resize(v->app->layouts.size()); v->draft.direct[index] = value(w, 231); } void layoutSelection(HWND w, SettingsView *v) { v->selection = (int)SendDlgItemMessageW(w, 220, LB_GETCURSEL, 0, 0); if (v->selection < 0) return; int i = v->order[v->selection]; auto &l = v->app->layouts[i]; check(w, 230, std::find(v->draft.cycle.begin(), v->draft.cycle.end(), l.id) != v->draft.cycle.end()); EnableWindow(GetDlgItem(w, 230), l.convertible); value(w, 231, i < (int)v->draft.direct.size() ? v->draft.direct[i] : L""); } void fillLayouts(HWND w, SettingsView *v) { SendDlgItemMessageW(w, 220, LB_RESETCONTENT, 0, 0); for (int i : v->order) { auto &l = v->app->layouts[i]; bool on = std::find(v->draft.cycle.begin(), v->draft.cycle.end(), l.id) != v->draft.cycle.end(); auto title = (on ? L"[✓] " : L"[ ] ") + l.name + (l.convertible ? L"" : L" — switch only"); SendDlgItemMessageW(w, 220, LB_ADDSTRING, 0, (LPARAM)title.c_str()); } SendDlgItemMessageW(w, 220, LB_SETCURSEL, std::max(0, v->selection), 0); layoutSelection(w, v); } void fillSettings(HWND w, SettingsView *v) { v->filling = true; auto &s = v->draft; check(w, 201, s.recent); check(w, 202, s.switchLayout); check(w, 208, s.writeLog); check(w, 209, s.screenshotAutoOpenEditor); check(w, 203, s.history); check(w, 204, s.autostart); check(w, 206, s.elevatedAutostart); check(w, 205, s.awake); check(w, 207, s.autoCorrect); value(w, 210, s.toggle); value(w, 211, s.casing); value(w, 212, s.translit); value(w, 213, s.undo); value(w, 214, s.clipboard); value(w, 215, s.screenshot); value(w, 240, std::to_wstring(s.maxItems)); value(w, 241, std::to_wstring(s.maxMiB)); value(w, 242, std::to_wstring(s.maxImageMiB)); std::wstring excluded; for (auto &name : s.excluded) { if (!excluded.empty()) excluded += L", "; excluded += name; } value(w, 243, excluded); v->order.clear(); for (auto &id : s.cycle) for (int i = 0; i < (int)v->app->layouts.size(); ++i) if (v->app->layouts[i].id == id) v->order.push_back(i); for (int i = 0; i < (int)v->app->layouts.size(); ++i) if (std::find(v->order.begin(), v->order.end(), i) == v->order.end()) v->order.push_back(i); v->selection = 0; fillLayouts(w, v); v->filling = false; } int integer(HWND w, int id) { auto s = value(w, id); size_t n{}; int v = std::stoi(s, &n); if (n != s.size()) throw std::runtime_error("Enter a whole number for history limits"); return v; } Settings collectSettings(HWND w, SettingsView *v) { collectLayout(w, v); auto s = v->draft; s.recent = checked(w, 201); s.switchLayout = checked(w, 202); s.writeLog = checked(w, 208); s.screenshotAutoOpenEditor = checked(w, 209); s.history = checked(w, 203); s.autostart = checked(w, 204); s.elevatedAutostart = checked(w, 206); s.awake = checked(w, 205); s.autoCorrect = checked(w, 207); s.toggle = value(w, 210); s.casing = value(w, 211); s.translit = value(w, 212); s.undo = value(w, 213); s.clipboard = value(w, 214); s.screenshot = value(w, 215); s.maxItems = integer(w, 240); s.maxMiB = integer(w, 241); s.maxImageMiB = integer(w, 242); s.excluded.clear(); std::wistringstream input(value(w, 243)); std::wstring item; while (std::getline(input, item, L',')) { auto first = item.find_first_not_of(L" \t"), last = item.find_last_not_of(L" \t"); if (first != item.npos) s.excluded.push_back(item.substr(first, last - first + 1)); } std::vector cycle; for (int i : v->order) { auto id = v->app->layouts[i].id; if (std::find(s.cycle.begin(), s.cycle.end(), id) != s.cycle.end()) cycle.push_back(id); } s.cycle = cycle; validateSettings(s); return s; } struct AutoOptionsView { HWND parent; SettingsView *settings; }; std::wstring commaList(const std::vector &items) { std::wstring text; for (auto &item : items) { if (!text.empty()) text += L", "; text += item; } return text; } std::vector parseCommaList(const std::wstring &text) { std::vector items; std::wistringstream input(text); std::wstring item; while (std::getline(input, item, L',')) { auto first = item.find_first_not_of(L" \t\r\n"), last = item.find_last_not_of(L" \t\r\n"); if (first != item.npos) items.push_back(item.substr(first, last - first + 1)); } return items; } LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { auto *view = (AutoOptionsView *)GetWindowLongPtrW(w, GWLP_USERDATA); if (!view) return DefWindowProcW(w, msg, wp, lp); if (msg == WM_COMMAND && LOWORD(wp) == 407) { auto folder = std::filesystem::path(executable()).parent_path() / L"dictionaries"; if ((INT_PTR)ShellExecuteW(w, L"open", folder.c_str(), nullptr, nullptr, SW_SHOWNORMAL) <= 32) MessageBoxW(w, L"Could not open the dictionary folder.", L"KSwitcher", MB_OK | MB_ICONERROR); return 0; } if (msg == WM_COMMAND && LOWORD(wp) == IDOK) { try { auto draft = view->settings->draft; auto lengthText = value(w, 410); if (lengthText.empty() || lengthText.size() > 2 || lengthText.find_first_not_of(L"0123456789") != lengthText.npos) throw std::runtime_error("Minimum word length must be between 1 and 32 letters"); draft.autoMinLength = std::stoi(lengthText); auto rankText = value(w, 406); if (rankText.empty() || rankText.find_first_not_of(L"0123456789") != rankText.npos) throw std::runtime_error("Enter a non-negative whole number for the frequency rank limit"); try { draft.autoMaxRank = std::stoi(rankText); } catch (...) { throw std::runtime_error("Frequency rank limit is too large"); } draft.autoLanguages.clear(); if (checked(w, 401)) draft.autoLanguages.push_back(L"en"); if (checked(w, 402)) draft.autoLanguages.push_back(L"uk"); if (checked(w, 403)) draft.autoLanguages.push_back(L"ru"); draft.autoNever = parseCommaList(value(w, 405)); for (auto [id, language] : {std::pair{411, L"en"}, {412, L"uk"}, {413, L"ru"}}) { auto text = lower(value(w, id)); std::replace(text.begin(), text.end(), L'\n', L','); auto words = parseCommaList(text); std::sort(words.begin(), words.end()); words.erase(std::unique(words.begin(), words.end()), words.end()); draft.autoWords[language] = std::move(words); } draft.autoExcluded = parseCommaList(value(w, 404)); draft.autoPrefixChars = value(w, 408); draft.autoSuffixChars = value(w, 409); draft.autoReplace = checked(w, 414); draft.autoTypos = checked(w, 415); draft.autoReplacements.clear(); std::wistringstream replacements(lower(value(w, 416))); std::wstring line; auto trim = [](std::wstring text) { auto first = text.find_first_not_of(L" \t\r\n"); return first == text.npos ? std::wstring{} : text.substr(first, text.find_last_not_of(L" \t\r\n") - first + 1); }; while (std::getline(replacements, line)) { if (trim(line).empty()) continue; auto equals = line.find(L'='); if (equals == line.npos) throw std::runtime_error("Enter one replacement per line: typo=correction"); if (!draft.autoReplacements.emplace(trim(line.substr(0, equals)), trim(line.substr(equals + 1))).second) throw std::runtime_error("Each replacement typo must occur only once"); } if (draft.autoLanguages.size() < 2) throw std::runtime_error("Select at least two languages"); validateSettings(draft); view->settings->draft.autoLanguages = draft.autoLanguages; view->settings->draft.autoNever = draft.autoNever; view->settings->draft.autoWords = draft.autoWords; view->settings->draft.autoExcluded = draft.autoExcluded; view->settings->draft.autoMaxRank = draft.autoMaxRank; view->settings->draft.autoMinLength = draft.autoMinLength; view->settings->draft.autoPrefixChars = draft.autoPrefixChars; view->settings->draft.autoSuffixChars = draft.autoSuffixChars; view->settings->draft.autoReplace = draft.autoReplace; view->settings->draft.autoTypos = draft.autoTypos; view->settings->draft.autoReplacements = std::move(draft.autoReplacements); DestroyWindow(w); } catch (const std::exception &e) { error(w, e); } return 0; } if ((msg == WM_COMMAND && LOWORD(wp) == IDCANCEL) || msg == WM_CLOSE) { DestroyWindow(w); return 0; } if (msg == WM_DPICHANGED) { resizeForDpi(w, wp, lp); return 0; } if (msg == WM_NCDESTROY) { EnableWindow(view->parent, TRUE); SetForegroundWindow(view->parent); disposeFont(w); delete view; SetWindowLongPtrW(w, GWLP_USERDATA, 0); } return DefWindowProcW(w, msg, wp, lp); } void showAutoOptions(HWND parent, SettingsView *v) { HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic correction", autoOptionsProc, 560, 880); windowLayoutState(w)->autoOptions = true; SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v}); label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525); checkbox(w, L"&English", 401, 18, 48, 150); checkbox(w, L"&Ukrainian", 402, 188, 48, 150); checkbox(w, L"&Russian", 403, 358, 48, 150); for (auto &lang : v->draft.autoLanguages) check(w, lang == L"en" ? 401 : lang == L"uk" ? 402 : 403, true); label(w, L"Excluded applications (process names, comma-separated)", 18, 94, 525); edit(w, 404, 18, 122, 524, 52, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL); value(w, 404, commaList(v->draft.autoExcluded)); label(w, L"Never correct these words (comma-separated)", 18, 184, 525); edit(w, 405, 18, 212, 524, 52, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL); value(w, 405, commaList(v->draft.autoNever)); label(w, L"My dictionary: correct words, separated by commas or new lines", 18, 276, 525); for (auto [id, language] : {std::pair{411, L"en"}, {412, L"uk"}, {413, L"ru"}}) { int x = 18 + (id - 411) * 178; label(w, id == 411 ? L"En&glish" : id == 412 ? L"U&krainian" : L"Ru&ssian", x, 302, 168); edit(w, id, x, 328, 168, 56, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL | ES_WANTRETURN); auto words = v->draft.autoWords.find(language); if (words != v->draft.autoWords.end()) value(w, id, commaList(words->second)); } label(w, L"Frequency rank limit (0 = entire dictionary)", 18, 400, 390); edit(w, 406, 420, 396, 122, 26, ES_NUMBER); value(w, 406, std::to_wstring(v->draft.autoMaxRank)); label(w, L"Minimum word length (1–32 letters)", 18, 434, 390); edit(w, 410, 420, 430, 122, 26, ES_NUMBER); value(w, 410, std::to_wstring(v->draft.autoMinLength)); label(w, L"Characters before a word", 18, 474, 250); label(w, L"Characters after a word", 290, 474, 250); edit(w, 408, 18, 500, 252); edit(w, 409, 290, 500, 252); value(w, 408, v->draft.autoPrefixChars); value(w, 409, v->draft.autoSuffixChars); label(w, L"Enter punctuation without spaces or separators. These characters are kept.", 18, 530, 525); checkbox(w, L"Use custom &replacements (take priority over dictionaries)", 414, 18, 562, 524); check(w, 414, v->draft.autoReplace); label(w, L"One pair per line: typo=correction (same language, letters only)", 18, 592, 525); edit(w, 416, 18, 618, 524, 70, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL | ES_WANTRETURN); std::wstring pairs; for (const auto &[from, to] : v->draft.autoReplacements) pairs += from + L"=" + to + L"\r\n"; value(w, 416, pairs); checkbox(w, L"Correct spelling &typos using dictionaries (experimental)", 415, 18, 700, 524); check(w, 415, v->draft.autoTypos); label(w, L"Single edits, 4+ letters; ambiguous words are left unchanged.", 18, 730, 525); button(w, L"Open dictionary &folder", 407, 18, 762, 220); label(w, L"Click OK, then Save in Settings. Enable automatic correction there.", 18, 800, 525); button(w, L"&OK", IDOK, 326, 832, 100); button(w, L"&Cancel", IDCANCEL, 440, 832, 100); RECT initial{}; GetWindowRect(w, &initial); initial.right += scale(w, 200); initial.bottom += scale(w, 140); fitToMonitor(w, initial); EnableWindow(parent, FALSE); showRequestedWindow(w); SetForegroundWindow(w); } struct OcrOptionsView { HWND parent; SettingsView *settings; }; Settings ocrDraft(HWND w, OcrOptionsView *v) { auto draft = v->settings->draft; draft.useTesseract = checked(w, 501); draft.tesseractPath = value(w, 502); draft.tesseractLanguages = value(w, 503); validateSettings(draft); return draft; } LRESULT CALLBACK ocrOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { auto *v = reinterpret_cast(GetWindowLongPtrW(w, GWLP_USERDATA)); if (!v) return DefWindowProcW(w, msg, wp, lp); if (msg == WM_COMMAND) { int id = LOWORD(wp); try { if (id == 507) { if ((INT_PTR)ShellExecuteW(w, L"open", L"https://github.com/UB-Mannheim/tesseract/wiki#tesseract-installer-for-windows", nullptr, nullptr, SW_SHOWNORMAL) <= 32) throw std::runtime_error("Could not open the Tesseract download page in your browser"); return 0; } if (id == 504) { wchar_t path[32768]{}; OPENFILENAMEW picker{sizeof(picker)}; picker.hwndOwner = w; picker.lpstrFile = path; picker.nMaxFile = std::size(path); picker.lpstrFilter = L"Tesseract executable (tesseract.exe)\0tesseract.exe\0Executables\0*.exe\0"; picker.Flags = OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; if (GetOpenFileNameW(&picker)) value(w, 502, path); return 0; } if (id == 505 || id == IDOK) { auto draft = ocrDraft(w, v); if (id == 505) { draft.useTesseract = true; validateTesseract(draft); SetDlgItemTextW(w, 506, (L"Ready: " + draft.tesseractLanguages).c_str()); return 0; } validateTesseract(draft); v->settings->draft.useTesseract = draft.useTesseract; v->settings->draft.tesseractPath = draft.tesseractPath; v->settings->draft.tesseractLanguages = draft.tesseractLanguages; DestroyWindow(w); return 0; } if (id == IDCANCEL) { DestroyWindow(w); return 0; } } catch (const std::exception &e) { error(w, e); return 0; } } if (msg == WM_CLOSE) { DestroyWindow(w); return 0; } if (msg == WM_NCDESTROY) { EnableWindow(v->parent, TRUE); SetForegroundWindow(v->parent); disposeFont(w); delete v; SetWindowLongPtrW(w, GWLP_USERDATA, 0); } return DefWindowProcW(w, msg, wp, lp); } void showOcrOptions(HWND parent, SettingsView *v) { HWND w = makeWindow(*v->app, L"KSwitcher.OcrOptions", L"Image text recognition", ocrOptionsProc, 640, 450); SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)new OcrOptionsView{parent, v}); label(w, L"Windows OCR is used by default. Recognition stays on this computer.", 18, 16, 600); checkbox(w, L"&Use optional Tesseract 5 instead of Windows OCR", 501, 18, 48, 600); check(w, 501, v->draft.useTesseract); label(w, L"Tesseract executable (blank = detect installed location)", 18, 90, 600); edit(w, 502, 18, 118, 484); value(w, 502, v->draft.tesseractPath); button(w, L"&Browse…", 504, 514, 117, 106); label(w, L"Languages used together (for example ukr+eng+rus)", 18, 160, 600); edit(w, 503, 18, 188, 602); value(w, 503, v->draft.tesseractLanguages); button(w, L"Check &installation", 505, 18, 228, 180); control(w, L"STATIC", L"", 0, 506, 212, 232, 408, 24); button(w, L"&Download Tesseract…", 507, 18, 266, 180); label(w, L"Opens the Windows installer page in your browser.", 212, 270, 408); label(w, L"Changing the engine or languages reprocesses saved images in the background.", 18, 314, 602); label(w, L"Click OK here, then Save in Settings to apply. No downloads are automatic.", 18, 344, 602); button(w, L"&OK", IDOK, 406, 390, 100); button(w, L"&Cancel", IDCANCEL, 520, 390, 100); EnableWindow(parent, FALSE); showRequestedWindow(w); SetForegroundWindow(w); } #include "ui_sync.inl" bool settingsChanged(HWND w, SettingsView *v) { try { return encodeSettings(collectSettings(w, v)) != encodeSettings(v->app->settings); } catch (const std::exception &) { // Invalid edits are unsaved changes too. Allow discarding them without // requiring the user to repair the fields first. return true; } } void closeSettings(HWND w, SettingsView *v) { if (settingsChanged(w, v)) { int choice = MessageBoxW(w, L"You have unsaved settings changes. Save them before closing?\n\n" L"Yes: save and close.\nNo: discard changes.\nCancel: keep editing.", L"KSwitcher — Unsaved settings", MB_YESNOCANCEL | MB_ICONWARNING | MB_DEFBUTTON3); if (choice != IDYES && choice != IDNO) return; if (choice == IDYES) { try { if (v->app->busy) throw std::runtime_error("Wait for the current text operation to finish"); v->app->applySettings(collectSettings(w, v)); } catch (const std::exception &e) { error(w, e); return; } } } DestroyWindow(w); } LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { if (msg == WM_DPICHANGED) { resizeForDpi(w, wp, lp); return 0; } auto *v = (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA); if (msg == WM_NCCREATE) { auto *app = (App *)((CREATESTRUCTW *)lp)->lpCreateParams; v = new SettingsView{app, app->settings}; SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v); return TRUE; } if (!v) return DefWindowProcW(w, msg, wp, lp); try { switch (msg) { case WM_COMMAND: { int id = LOWORD(wp); if (id == 220 && HIWORD(wp) == LBN_SELCHANGE && !v->filling) { collectLayout(w, v); layoutSelection(w, v); return 0; } if (id == 256) { showLogs(*v->app); return 0; } if (id == 257) { showOcrOptions(w, v); return 0; } if (id == 258) { showSyncOptions(w, *v->app); return 0; } if (id == 230) { collectLayout(w, v); fillLayouts(w, v); return 0; } if (id == 232 || id == 233) { collectLayout(w, v); int next = v->selection + (id == 232 ? -1 : 1); if (next >= 0 && next < (int)v->order.size()) { std::swap(v->order[next], v->order[v->selection]); v->selection = next; fillLayouts(w, v); } return 0; } if (id == IDOK) { if (v->app->busy) throw std::runtime_error("Wait for the current text operation to finish"); v->app->applySettings(collectSettings(w, v)); DestroyWindow(w); return 0; } if (id == IDCANCEL) { closeSettings(w, v); return 0; } if (id == 250) { auto path = pickFile(w, false); if (!path.empty()) { v->draft = loadSettings(path, v->app->layouts); fillSettings(w, v); } return 0; } if (id == 251) { auto s = collectSettings(w, v); auto path = pickFile(w, true); if (!path.empty()) saveSettings(path, s); return 0; } if (id == 255) { showAutoOptions(w, v); return 0; } if (id == 252) { showSnippets(*v->app); return 0; } if (id == 253) { if (v->app->busy) throw std::runtime_error("Wait for the current operation"); v->draft = collectSettings(w, v); v->app->reloadLayouts(); v->draft.direct = v->app->settings.direct; fillSettings(w, v); return 0; } if (id == 254) { if (MessageBoxW(w, L"Delete all clipboard history, including pinned items?", L"Clear history", MB_YESNO | MB_ICONWARNING) == IDYES) v->app->clearHistory(); return 0; } break; } case WM_CLOSE: closeSettings(w, v); return 0; case WM_NCDESTROY: disposeFont(w); v->app->settingsWindow = nullptr; delete v; return DefWindowProcW(w, msg, wp, lp); } } catch (const std::exception &e) { error(w, e); } return DefWindowProcW(w, msg, wp, lp); } struct SnippetView { App *app; std::vector snippets; int selection = -1; }; void updateSnippetEditor(HWND w, SnippetView *v) { bool selected = v->selection >= 0 && v->selection < (int)v->snippets.size(); value(w, 302, selected ? v->snippets[v->selection].trigger : L""); value(w, 303, selected ? v->snippets[v->selection].text : L""); for (int id : {302, 303, 311, 312, 313}) EnableWindow(GetDlgItem(w, id), selected); } void fillSnippets(HWND w, SnippetView *v) { SendDlgItemMessageW(w, 301, LB_RESETCONTENT, 0, 0); for (auto &sn : v->snippets) SendDlgItemMessageW(w, 301, LB_ADDSTRING, 0, (LPARAM)sn.trigger.c_str()); if (v->selection >= 0) SendDlgItemMessageW(w, 301, LB_SETCURSEL, v->selection, 0); updateSnippetEditor(w, v); } void collectSnippet(HWND w, SnippetView *v) { if (v->selection >= 0 && v->selection < (int)v->snippets.size()) { auto &snippet = v->snippets[v->selection]; if (snippet.id.empty()) snippet.id = snippetId(snippet); snippet.trigger = value(w, 302); snippet.text = value(w, 303); } } LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { if (msg == WM_DPICHANGED) { resizeForDpi(w, wp, lp); return 0; } auto *v = (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA); if (msg == WM_NCCREATE) { auto *app = (App *)((CREATESTRUCTW *)lp)->lpCreateParams; v = new SnippetView{app, app->settings.snippets}; SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v); return TRUE; } if (!v) return DefWindowProcW(w, msg, wp, lp); try { switch (msg) { case WM_COMMAND: { int id = LOWORD(wp); if (id == 301 && HIWORD(wp) == LBN_SELCHANGE) { collectSnippet(w, v); v->selection = (int)SendDlgItemMessageW(w, 301, LB_GETCURSEL, 0, 0); updateSnippetEditor(w, v); return 0; } if (id == 310) { collectSnippet(w, v); v->snippets.push_back({L";new", L"", newSnippetId()}); v->selection = (int)v->snippets.size() - 1; fillSnippets(w, v); SetFocus(GetDlgItem(w, 302)); return 0; } if (id == 311) { if (v->selection >= 0) { v->snippets.erase(v->snippets.begin() + v->selection); v->selection = -1; fillSnippets(w, v); SetFocus(GetDlgItem(w, 301)); } return 0; } if (id == 312 || id == 313) { if (v->selection < 0 || v->selection >= (int)v->snippets.size()) return 0; SendDlgItemMessageW(w, 303, EM_REPLACESEL, TRUE, (LPARAM)(id == 312 ? L"{cursor}" : L"{enter}")); return 0; } if (id == IDOK) { collectSnippet(w, v); auto s = v->app->settings; s.snippets = v->snippets; if (v->app->busy) throw std::runtime_error("Wait for the current operation"); v->app->applySettings(s); if (v->app->settingsWindow) { auto *parent = (SettingsView *)GetWindowLongPtrW(v->app->settingsWindow, GWLP_USERDATA); parent->draft.snippets = s.snippets; } DestroyWindow(w); return 0; } if (id == IDCANCEL) { DestroyWindow(w); return 0; } break; } case WM_CLOSE: DestroyWindow(w); return 0; case WM_NCDESTROY: disposeFont(w); v->app->snippetsWindow = nullptr; delete v; return DefWindowProcW(w, msg, wp, lp); } } catch (const std::exception &e) { error(w, e); } return DefWindowProcW(w, msg, wp, lp); } #endif #ifndef KS_CONFIG_APP LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { if (msg == WM_SIZE) { MoveWindow(GetDlgItem(w, 1), 10, 10, LOWORD(lp) - 20, HIWORD(lp) - 20, TRUE); return 0; } if (msg == WM_NCDESTROY) disposeFont(w); return DefWindowProcW(w, msg, wp, lp); } #endif } // namespace #ifndef KS_CONFIG_APP void refreshHistory(App &app) { if (app.historyWindow) { auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA); search(app.historyWindow, v); } } void showHistory(App &app, bool visible) { if (!app.history) { app.report(L"Clipboard history is unavailable. See the log in the data folder."); return; } HWND foreground = GetForegroundWindow(); if (visible && app.historyWindow && IsWindowVisible(app.historyWindow) && foreground == app.historyWindow) { auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA); if (v->items.size() > 1) app.pasteEntry(v->items[1].id, false); return; } if (foreground != app.window && foreground != app.historyWindow) app.pasteTarget = foreground; if (!app.historyWindow) { HWND w = makeWindow(app, L"KSwitcher.History", L"Clipboard history", historyProc, 760, 500); app.historyWindow = w; windowLayoutState(w)->history = true; ((HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA))->rich = createRichText(w, 105, false); label(w, L"&Search clipboard history", 16, 12, 500); edit(w, 101, 16, 38, 728); auto list = control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS, 102, 16, 76, 728, 360); SendMessageW(list, LB_SETITEMHEIGHT, 0, scale(w, 88)); button(w, L"&Paste", 110, 16, 452, 90); button(w, L"Plain &text", 111, 114, 452, 100); button(w, L"P&in / unpin", 112, 222, 452, 110); button(w, L"&Delete", 113, 340, 452, 90); button(w, L"&Open item", 114, 438, 452, 100); button(w, L"Clear &history…", 116, 546, 452, 150); SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w); SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w); } if (!visible) return; POINT cursor; GetCursorPos(&cursor); MONITORINFO monitor{sizeof(monitor)}; GetMonitorInfoW(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &monitor); RECT r; GetWindowRect(app.historyWindow, &r); int x = std::max(monitor.rcWork.left, std::min(cursor.x, monitor.rcWork.right - (r.right - r.left))); int y = std::max(monitor.rcWork.top, std::min(cursor.y, monitor.rcWork.bottom - (r.bottom - r.top))); SetWindowPos(app.historyWindow, HWND_TOP, x, y, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW); SetForegroundWindow(app.historyWindow); SetFocus(GetDlgItem(app.historyWindow, 101)); refreshHistory(app); } #endif #ifndef KS_RESIDENT_APP #include "ui_log.inl" #else void showLogs(App &app) { launchConfiguration(app, false, true); } #endif #ifndef KS_RESIDENT_APP void showSettings(App &app, bool visible) { if (app.settingsWindow) { ShowWindow(app.settingsWindow, SW_RESTORE); HWND target = app.settingsWindow; if (!IsWindowEnabled(target)) target = GetLastActivePopup(target); activateWindow(target); return; } HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 780, 660, 0, false); app.settingsWindow = w; label(w, L"Text conversion", 18, 16); checkbox(w, L"Con&vert recently typed text when nothing is selected", 201, 18, 43, 390); checkbox(w, L"Switch Windows input &layout after conversion", 202, 18, 70, 390); checkbox(w, L"Record clip&board history", 203, 18, 106); checkbox(w, L"Start with &Windows", 204, 18, 133); checkbox(w, L"Start with Windows as ad&ministrator", 206, 18, 160, 395); checkbox(w, L"&Keep screen awake", 205, 18, 184, 210); checkbox(w, L"Write lo&g", 208, 240, 184, 160); label(w, L"Click a shortcut and press the desired keys", 420, 16, 350); const wchar_t *names[] = {L"Next layout", L"Change case", L"Transliterate", L"Undo", L"Clipboard history", L"Screen capture"}; for (int i = 0; i < 6; ++i) { label(w, names[i], 420, 48 + i * 30, 115); hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220); } checkbox(w, L"Open editor after screen capt&ure", 209, 420, 224, 340); checkbox(w, L"Automatic correction (e&xperimental)", 207, 18, 210, 395); label(w, L"Layout cycle — top to bottom", 18, 240, 400); control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220, 18, 262, 470, 172); button(w, L"Move u&p", 232, 504, 262, 120); button(w, L"Move &down", 233, 632, 262, 128); checkbox(w, L"&Include in cycle", 230, 504, 300, 240); label(w, L"Direct layout shortcut", 504, 332, 240); hotkeyEdit(app, w, 231, 504, 356, 256); label(w, L"Backspace / Delete clears a shortcut", 504, 382, 256); button(w, L"Re&fresh layouts", 253, 504, 408, 150); label(w, L"History limits", 18, 444); label(w, L"Items", 18, 478, 50); edit(w, 240, 70, 474, 90); label(w, L"Total MiB", 180, 478, 75); edit(w, 241, 260, 474, 90); label(w, L"Per item MiB", 370, 478, 100); edit(w, 242, 475, 474, 80); button(w, L"Clear &history…", 254, 580, 474, 180); label(w, L"Exclude applications (process names, comma-separated)", 18, 516, 700); edit(w, 243, 18, 544, 742); button(w, L"&Auto-correction options…", 255, 18, 575, 240); button(w, L"View l&og…", 256, 278, 575, 220); button(w, L"OCR op&tions…", 257, 518, 575, 242); button(w, L"Impo&rt settings…", 250, 18, 606, 145); button(w, L"&Export settings…", 251, 173, 606, 145); button(w, L"S&nippets…", 252, 328, 606, 110); button(w, L"S&ync...", 258, 448, 606, 90); button(w, L"&Save", IDOK, 548, 606, 100); button(w, L"&Cancel", IDCANCEL, 660, 606, 100); fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA)); if (visible) { showRequestedWindow(w); activateWindow(w); } } void showSnippets(App &app) { if (app.snippetsWindow) { SetForegroundWindow(app.snippetsWindow); return; } HWND w = makeWindow(app, L"KSwitcher.Snippets", L"Text snippets", snippetProc, 740, 520); app.snippetsWindow = w; label(w, L"Triggers", 16, 16); control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 301, 16, 44, 200, 380); button(w, L"&Add", 310, 16, 438, 90); button(w, L"&Delete", 311, 116, 438, 100); label(w, L"&Trigger (for example ;addr)", 236, 16, 470); edit(w, 302, 236, 44, 488); label(w, L"&Replacement text", 236, 82, 480); edit(w, 303, 236, 110, 488, 270, ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL | WS_VSCROLL); control(w, L"STATIC", L"{cursor}: caret; {enter}: press Enter after insertion.\nUse {{cursor}} / {{enter}} for literal markers.", 0, 0, 236, 392, 488, 36); button(w, L"Insert cursor &marker", 312, 236, 430, 190); button(w, L"Insert &Enter marker", 313, 436, 430, 190); button(w, L"&Save", IDOK, 510, 478, 100); button(w, L"&Cancel", IDCANCEL, 624, 478, 100); fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA)); showRequestedWindow(w); SetForegroundWindow(w); } #else void showSettings(App &app, bool) { launchConfiguration(app); } void showSnippets(App &app) { launchConfiguration(app, true); } #endif #ifndef KS_CONFIG_APP void showShortcuts(App &app) { auto &s = app.settings; std::wstring text = L"Next layout: " + s.toggle + L"\nChange case: " + s.casing + L"\nTransliterate: " + s.translit + L"\nUndo: " + s.undo + L"\nClipboard history: " + s.clipboard + L"\nScreen capture: " + s.screenshot + L"\n"; for (size_t i = 0; i < std::min(app.layouts.size(), s.direct.size()); ++i) if (!s.direct[i].empty()) text += L"\n" + app.layouts[i].name + L": " + s.direct[i]; text += L"\n\nHistory: Enter to paste, Shift+Enter for plain text,\nCtrl+1…9 to paste, Ctrl+P to pin, " L"Delete to remove,\nCtrl+O to open the full item, Escape to close."; MessageBoxW(app.window, text.c_str(), L"Keyboard shortcuts", MB_OK); } void showEntry(App &app, const std::wstring &id) { try { auto clip = app.history->load(id); if(std::any_of(clip.formats.begin(),clip.formats.end(),[](const auto &format) { return format.id==CF_DIB || format.id==CF_DIBV5 || format.name==L"PNG"; })) { launchScreenshotEditor(app,clip); return; } auto text = clip.text(); if (text.empty()) { MessageBoxW(app.historyWindow, L"This item contains files. Paste it into the " L"destination application.", L"Clipboard item", MB_OK); return; } HWND w = makeWindow(app, L"KSwitcher.Entry", L"Clipboard item", entryProc, 720, 500, WS_THICKFRAME | WS_MAXIMIZEBOX); auto field = edit(w, 1, 10, 10, 700, 480, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL); SetWindowTextW(field, text.c_str()); showRequestedWindow(w); } catch (const std::exception &e) { error(app.historyWindow, e); } } #endif } // namespace ks