#include "../resources/resource.h" #include "app.h" #include "ocr.h" #include "richtext.h" #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"); } 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); 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->hotkeyCapture = w; SendMessageW(w, EM_SETSEL, 0, -1); } if (msg == WM_KILLFOCUS || msg == WM_NCDESTROY) { if (app->hotkeyCapture == w) app->hotkeyCapture = 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); } 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); } 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; } void value(HWND w, int id, const std::wstring &text) { SetDlgItemTextW(w, id, text.c_str()); } bool checked(HWND w, int id) { return IsDlgButtonChecked(w, id) == BST_CHECKED; } 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) { 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 | extra; AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi); HWND w = CreateWindowExW(WS_EX_APPWINDOW, cls, title, style, CW_USEDEFAULT, CW_USEDEFAULT, 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"))); SetWindowTextW(w, title); 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) { 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}; 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); SetPropW(w, L"Font", replacement); SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)next); if (old) DeleteObject(old); auto *r = (RECT *)lp; SetWindowPos(w, nullptr, r->left, r->top, r->right - r->left, r->bottom - r->top, SWP_NOZORDER); } 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); } 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; }; 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->enqueue( [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) { 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 WM_COMMAND: { int id = LOWORD(wp); if (id == 101 && HIWORD(wp) == EN_CHANGE) { SetTimer(w, 1, 180, nullptr); return 0; } if (id == 102 && HIWORD(wp) == LBN_SELCHANGE) { return 0; } if (id == 102 && HIWORD(wp) == LBN_DBLCLK) id = 110; 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); v->items = std::move(result->items); SendDlgItemMessageW(w, 102, WM_SETREDRAW, FALSE, 0); SendDlgItemMessageW(w, 102, LB_RESETCONTENT, 0, 0); int select = 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; } SendDlgItemMessageW(w, 102, LB_SETCURSEL, select, 0); 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); // At most ~3.3 MiB, irrespective of history length or source image size. if (v->thumbnails.size() >= 32) v->thumbnails.erase(v->thumbnails.begin()); if (v->richTexts.size() >= 32) v->richTexts.erase(v->richTexts.begin()); if (!result->rtf.empty()) v->richTexts[result->id] = std::move(result->rtf); else v->thumbnails[result->id] = std::move(result->pixels); InvalidateRect(GetDlgItem(w, 102), nullptr, 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: { 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"Pin / unpin"); AppendMenuW(menu, MF_STRING, 113, L"Delete"); AppendMenuW(menu, MF_STRING, 114, L"Open full item"); AppendMenuW(menu, MF_STRING, 115, L"Copy text from image (Windows OCR)"); POINT p; GetCursorPos(&p); 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_DPICHANGED: { resizeForDpi(w, wp, lp); SendDlgItemMessageW(w, 102, LB_SETITEMHEIGHT, 0, scale(w, 88)); 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); } 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, 203, s.history); check(w, 204, s.autostart); check(w, 206, s.elevatedAutostart); check(w, 205, s.awake); 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, 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.history = checked(w, 203); s.autostart = checked(w, 204); s.elevatedAutostart = checked(w, 206); s.awake = checked(w, 205); 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.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; } 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 == 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) { DestroyWindow(w); 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 == 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 && v->app->history) { if (MessageBoxW(w, L"Delete all clipboard history, including pinned items?", L"Clear history", MB_YESNO | MB_ICONWARNING) == IDYES) v->app->history->clear(); return 0; } break; } case WM_CLOSE: DestroyWindow(w); 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 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); } void collectSnippet(HWND w, SnippetView *v) { if (v->selection >= 0 && v->selection < (int)v->snippets.size()) { v->snippets[v->selection] = {value(w, 302), 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); if (v->selection >= 0) { value(w, 302, v->snippets[v->selection].trigger); value(w, 303, v->snippets[v->selection].text); } return 0; } if (id == 310) { collectSnippet(w, v); v->snippets.push_back({L";new", L""}); v->selection = (int)v->snippets.size() - 1; fillSnippets(w, v); value(w, 302, L";new"); value(w, 303, L""); 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); value(w, 302, L""); value(w, 303, L""); } return 0; } if (id == 312) { SendDlgItemMessageW(w, 303, EM_REPLACESEL, TRUE, (LPARAM)L"{cursor}"); 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); } 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); } } // namespace void refreshHistory(App &app) { if (app.historyWindow) { auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA); search(app.historyWindow, v); } } void showHistory(App &app) { if (!app.history) { app.report(L"Clipboard history is unavailable. See the log in the data folder."); return; } HWND foreground = GetForegroundWindow(); if (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; ((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"Pin / unpin", 112, 222, 452, 110); button(w, L"Delete", 113, 340, 452, 90); button(w, L"Open item", 114, 438, 452, 100); SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w); SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w); } 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); } void showSettings(App &app) { if (app.settingsWindow) { ShowWindow(app.settingsWindow, SW_RESTORE); SetForegroundWindow(app.settingsWindow); return; } HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 780, 660); app.settingsWindow = w; label(w, L"Text conversion", 18, 16); checkbox(w, L"Convert recently typed text when nothing is selected", 201, 18, 43, 440); checkbox(w, L"Switch Windows input layout after conversion", 202, 18, 70, 440); checkbox(w, L"Record clipboard history", 203, 18, 106); checkbox(w, L"Start with Windows", 204, 18, 133); checkbox(w, L"Start with Windows as administrator", 206, 18, 160, 395); checkbox(w, L"Keep screen awake", 205, 18, 184); 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"}; for (int i = 0; i < 5; ++i) { label(w, names[i], 420, 48 + i * 30, 115); hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220); } label(w, L"Layout cycle — top to bottom", 18, 210, 400); control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220, 18, 232, 470, 190); button(w, L"Move up", 232, 504, 232, 120); button(w, L"Move down", 233, 632, 232, 128); checkbox(w, L"Include in cycle", 230, 504, 275, 240); label(w, L"Direct layout shortcut", 504, 313, 240); hotkeyEdit(app, w, 231, 504, 340, 256); label(w, L"Backspace / Delete clears a shortcut", 504, 365, 256); button(w, L"Refresh layouts", 253, 504, 384, 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"Import settings…", 250, 18, 606, 145); button(w, L"Export settings…", 251, 173, 606, 145); button(w, L"Snippets…", 252, 328, 606, 110); button(w, L"Save", IDOK, 548, 606, 100); button(w, L"Cancel", IDCANCEL, 660, 606, 100); fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA)); ShowWindow(w, SW_SHOW); SetForegroundWindow(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); label(w, L"Use {cursor} once; {{cursor}} inserts the literal marker.", 236, 392, 488); button(w, L"Insert cursor marker", 312, 236, 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)); ShowWindow(w, SW_SHOW); SetForegroundWindow(w); } 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"\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); auto text = clip.text(); if (text.empty()) { MessageBoxW(app.historyWindow, L"This item contains an image or files. Use the preview or 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()); ShowWindow(w, SW_SHOW); } catch (const std::exception &e) { error(app.historyWindow, e); } } } // namespace ks