Files
hrynco-kswitcher/src/ui.cpp
T

1740 lines
64 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "../resources/resource.h"
#include "app.h"
#include "ocr.h"
#include "richtext.h"
#include "ui_geometry.h"
#include <algorithm>
#include <commctrl.h>
#include <commdlg.h>
#include <map>
#include <richedit.h>
#include <set>
#include <shellapi.h>
#include <sstream>
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{};
int wheelRemainder{}, horizontalWheelRemainder{};
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
};
WindowLayout *windowLayoutState(HWND w)
{
return reinterpret_cast<WindowLayout *>(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);
}
for (const auto &[child, r] : layout->controls)
SetWindowPos(child, nullptr, scale(w, r.left) - layout->x, scale(w, r.top) - layout->y,
scale(w, r.right - r.left), scale(w, r.bottom - r.top), SWP_NOACTIVATE | SWP_NOZORDER);
layout->arranging = false;
InvalidateRect(w, nullptr, TRUE);
}
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<POINT *>(&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;
}
arrangeWindow(w);
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;
(horizontal ? layout->x : layout->y) += steps * scale(w, 48) * (msg == WM_MOUSEHWHEEL ? 1 : -1);
arrangeWindow(w);
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 (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->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 | WS_THICKFRAME | WS_MAXIMIZEBOX | extra;
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);
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, 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)
{
// 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);
fitToMonitor(w, *(RECT *)lp);
arrangeWindow(w);
}
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<Entry> items;
};
struct PreviewResult
{
uint64_t generation;
std::wstring id;
Pixels pixels;
std::string rtf;
};
struct HistoryView
{
App *app{};
std::vector<Entry> items;
uint64_t generation{};
std::map<std::wstring, Pixels> thumbnails;
std::map<std::wstring, std::string> richTexts;
HWND rich{};
std::set<std::wstring> 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<SearchResult> 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<PreviewResult> 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<int> 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, 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, 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.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.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<std::wstring> 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<std::wstring> &items)
{
std::wstring text;
for (auto &item : items)
{
if (!text.empty())
text += L", ";
text += item;
}
return text;
}
std::vector<std::wstring> parseCommaList(const std::wstring &text)
{
std::vector<std::wstring> 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));
draft.autoExcluded = parseCommaList(value(w, 404));
draft.autoPrefixChars = value(w, 408);
draft.autoSuffixChars = value(w, 409);
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.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;
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 layout correction", autoOptionsProc,
560, 680);
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, 84, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL);
value(w, 404, commaList(v->draft.autoExcluded));
label(w, L"Personal dictionary: never correct these words (comma-separated)", 18, 220, 525);
edit(w, 405, 18, 248, 524, 84, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL);
value(w, 405, commaList(v->draft.autoNever));
label(w, L"Corrects after Space and a short pause.", 18, 344, 525);
label(w, L"Undo skips this word until KSwitcher restarts.", 18, 366, 525);
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);
button(w, L"Open dictionary folder", 407, 18, 562, 220);
label(w, L"After editing files, click OK here, then Save in Settings to reload.", 18, 600, 525);
button(w, L"OK", IDOK, 326, 632, 100);
button(w, L"Cancel", IDCANCEL, 440, 632, 100);
EnableWindow(parent, FALSE);
showRequestedWindow(w);
SetForegroundWindow(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 == 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 == 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 && 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<Snippet> 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);
}
namespace
{
constexpr int LogChoice = 601, LogRows = 602, LogRefresh = 603, LogStatus = 604, LogCopy = 605,
LogClear = 606;
LRESULT CALLBACK logListProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
{
// Header notifications go to the ListView, not its top-level parent.
if (msg == WM_NOTIFY && ((NMHDR *)lp)->hwndFrom == ListView_GetHeader(w) &&
((NMHDR *)lp)->code == NM_CUSTOMDRAW)
{
auto *draw = (NMCUSTOMDRAW *)lp;
if (draw->dwDrawStage == CDDS_PREPAINT)
{
FillRect(draw->hdc, &draw->rc, GetSysColorBrush(COLOR_BTNFACE));
return CDRF_NOTIFYITEMDRAW;
}
if (draw->dwDrawStage == CDDS_ITEMPREPAINT)
{
int saved = SaveDC(draw->hdc);
FillRect(draw->hdc, &draw->rc, GetSysColorBrush(COLOR_BTNFACE));
RECT edge = draw->rc;
edge.top = edge.bottom - 1;
FillRect(draw->hdc, &edge, GetSysColorBrush(COLOR_3DSHADOW));
edge = draw->rc;
edge.left = edge.right - 1;
FillRect(draw->hdc, &edge, GetSysColorBrush(COLOR_3DSHADOW));
wchar_t title[128]{};
HDITEMW item{};
item.mask = HDI_TEXT;
item.pszText = title;
item.cchTextMax = 128;
Header_GetItem(draw->hdr.hwndFrom, (int)draw->dwItemSpec, &item);
auto headerFont = (HFONT)SendMessageW(draw->hdr.hwndFrom, WM_GETFONT, 0, 0);
if (headerFont)
SelectObject(draw->hdc, headerFont);
SetBkMode(draw->hdc, TRANSPARENT);
SetTextColor(draw->hdc, GetSysColor(COLOR_BTNTEXT));
RECT text = draw->rc;
InflateRect(&text, -scale(w, 6), 0);
DrawTextW(draw->hdc, title, -1, &text, DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS | DT_NOPREFIX);
RestoreDC(draw->hdc, saved);
return CDRF_SKIPDEFAULT;
}
}
if (msg == WM_NCDESTROY)
RemoveWindowSubclass(w, logListProc, 1);
return DefSubclassProc(w, msg, wp, lp);
}
void copyLogRows(HWND w, App &app)
{
HWND list = GetDlgItem(w, LogRows);
std::wstring copied;
for (int row = ListView_GetNextItem(list, -1, LVNI_SELECTED); row >= 0;
row = ListView_GetNextItem(list, row, LVNI_SELECTED))
{
for (int column = 0; column < 4; ++column)
{
std::vector<wchar_t> text(32768);
ListView_GetItemText(list, row, column, text.data(), (int)text.size());
if (column)
copied += L'\t';
copied += text.data();
}
copied += L"\r\n";
}
if (!copied.empty())
app.ownSequence = writeClipboard(app.window, textClip(copied));
}
void refreshLogView(HWND w, App &app)
{
const bool previous = SendDlgItemMessageW(w, LogChoice, CB_GETCURSEL, 0, 0) == 1;
std::wstring contents;
try
{
contents = readDiagnosticLog(previous);
}
catch (const std::exception &e)
{
contents = L"Could not read the log: " + wide(e.what());
}
HWND list = GetDlgItem(w, LogRows);
SendMessageW(list, WM_SETREDRAW, FALSE, 0);
ListView_DeleteAllItems(list);
std::wistringstream lines(contents);
std::wstring line, timestamp;
int row = 0;
while (std::getline(lines, line))
{
if (!line.empty() && line.back() == L'\r')
line.pop_back();
if (line.empty())
continue;
auto firstSpace = line.find(L' ');
auto secondSpace = firstSpace == line.npos ? line.npos : line.find(L' ', firstSpace + 1);
if (iswdigit(line.front()) && line.find(L'-') < firstSpace && secondSpace != line.npos)
{
timestamp = line.substr(0, secondSpace);
line.erase(0, secondSpace + 1);
}
auto id = line.find(L" id=");
std::wstring attempt;
if (id != line.npos)
{
auto end = line.find(L' ', id + 4);
attempt = line.substr(id + 4, end == line.npos ? end : end - id - 4);
}
std::wstring event = line;
if (event.starts_with(L" ageMs="))
{
auto end = event.find(L' ', 8);
if (end != event.npos)
event.erase(0, end + 1);
}
auto equals = event.find(L'=');
if (equals != event.npos)
{
auto key = event.rfind(L' ', equals);
if (key != event.npos)
event.resize(key);
}
LVITEMW item{};
item.mask = LVIF_TEXT;
item.iItem = row;
item.pszText = timestamp.data();
ListView_InsertItem(list, &item);
ListView_SetItemText(list, row, 1, attempt.data());
ListView_SetItemText(list, row, 2, event.data());
ListView_SetItemText(list, row, 3, line.data());
++row;
}
SendMessageW(list, WM_SETREDRAW, TRUE, 0);
InvalidateRect(list, nullptr, TRUE);
if (row)
ListView_EnsureVisible(list, row - 1, FALSE);
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: problems and suspicious repeats only."
: L"Logging disabled. Showing previously saved entries.");
}
LRESULT CALLBACK logProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
auto *app = reinterpret_cast<App *>(GetWindowLongPtrW(w, GWLP_USERDATA));
if (msg == WM_NCCREATE)
{
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)((CREATESTRUCTW *)lp)->lpCreateParams);
return TRUE;
}
if (!app)
return DefWindowProcW(w, msg, wp, lp);
switch (msg)
{
case WM_COMMAND:
if ((LOWORD(wp) == LogRefresh && HIWORD(wp) == BN_CLICKED) ||
(LOWORD(wp) == LogChoice && HIWORD(wp) == CBN_SELCHANGE))
{
refreshLogView(w, *app);
return 0;
}
if (LOWORD(wp) == LogCopy && HIWORD(wp) == BN_CLICKED)
{
try { copyLogRows(w, *app); }
catch (const std::exception &e) { error(w, e); }
return 0;
}
if (LOWORD(wp) == LogClear && HIWORD(wp) == BN_CLICKED)
{
try
{
clearDiagnosticLog(SendDlgItemMessageW(w, LogChoice, CB_GETCURSEL, 0, 0) == 1);
refreshLogView(w, *app);
}
catch (const std::exception &e) { error(w, e); }
return 0;
}
if (LOWORD(wp) != IDCANCEL || HIWORD(wp) != BN_CLICKED)
break;
[[fallthrough]];
case WM_CLOSE:
DestroyWindow(w);
return 0;
case WM_NOTIFY:
if (((NMHDR *)lp)->idFrom == LogRows && ((NMHDR *)lp)->code == LVN_KEYDOWN)
{
auto key = ((NMLVKEYDOWN *)lp)->wVKey;
if (GetKeyState(VK_CONTROL) & 0x8000)
{
if (key == 'A')
ListView_SetItemState(GetDlgItem(w, LogRows), -1, LVIS_SELECTED, LVIS_SELECTED);
if (key == 'C')
{
try { copyLogRows(w, *app); }
catch (const std::exception &e) { error(w, e); }
}
}
}
return 0;
case WM_DPICHANGED:
resizeForDpi(w, wp, lp);
return 0;
case WM_NCDESTROY:
app->logWindow = nullptr;
disposeFont(w);
break;
}
return DefWindowProcW(w, msg, wp, lp);
}
}
void showLogs(App &app)
{
if (app.logWindow)
{
showRequestedWindow(app.logWindow);
activateWindow(app.logWindow);
return;
}
auto w = makeWindow(app, L"KSwitcher.Logs", L"KSwitcher log", logProc, 900, 590);
app.logWindow = w;
auto choice = control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST, LogChoice, 16, 16, 300, 140);
SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Current log — kswitcher.log");
SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Previous log — kswitcher.log.1");
SendMessageW(choice, CB_SETCURSEL, 0, 0);
button(w, L"Refresh", LogRefresh, 330, 14, 110);
button(w, L"Copy selected", LogCopy, 454, 14, 140);
button(w, L"Clear log", LogClear, 608, 14, 110);
control(w, L"STATIC", L"", 0, LogStatus, 16, 52, 860, 22);
INITCOMMONCONTROLSEX common{sizeof(common), ICC_LISTVIEW_CLASSES};
InitCommonControlsEx(&common);
auto list = control(w, WC_LISTVIEWW, L"", WS_TABSTOP | WS_BORDER | LVS_REPORT | LVS_SHOWSELALWAYS,
LogRows, 16, 82, 868, 454);
ListView_SetExtendedListViewStyle(list, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
SetWindowSubclass(list, logListProc, 1, 0);
const wchar_t *titles[] = {L"Time", L"Attempt", L"Event", L"Details"};
const int widths[] = {155, 65, 195, 900};
for (int i = 0; i < 4; ++i)
{
LVCOLUMNW column{};
column.mask = LVCF_TEXT | LVCF_WIDTH;
column.pszText = const_cast<wchar_t *>(titles[i]);
column.cx = scale(w, widths[i]);
ListView_InsertColumn(list, i, &column);
}
label(w, L"Select rows to copy. Ctrl+A selects all; Ctrl+C copies selected rows.", 16, 551, 720);
button(w, L"Close", IDCANCEL, 774, 548, 110);
refreshLogView(w, app);
showRequestedWindow(w);
activateWindow(w);
}
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);
app.settingsWindow = w;
label(w, L"Text conversion", 18, 16);
checkbox(w, L"Convert 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 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, 210);
checkbox(w, L"Write log", 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"};
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);
}
checkbox(w, L"Automatically correct layout (experimental)", 207, 420, 195, 350);
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"Auto-correction options…", 255, 18, 575, 240);
button(w, L"View log…", 256, 278, 575, 220);
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));
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);
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));
showRequestedWindow(w);
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());
showRequestedWindow(w);
}
catch (const std::exception &e)
{
error(app.historyWindow, e);
}
}
} // namespace ks