1156 lines
41 KiB
C++
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
|