Files
hrynco-kswitcher/src/ui.cpp
T

1156 lines
41 KiB
C++

#include "../resources/resource.h"
#include "app.h"
#include "ocr.h"
#include "richtext.h"
#include <algorithm>
#include <commctrl.h>
#include <commdlg.h>
#include <map>
#include <richedit.h>
#include <set>
#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");
}
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<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, 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<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;
}
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<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);
}
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