Files
hrynco-kswitcher/src/ui.cpp
T

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