1740 lines
64 KiB
C++
1740 lines
64 KiB
C++
#include "../resources/resource.h"
|
||
#include "app.h"
|
||
#include "ocr.h"
|
||
#include "richtext.h"
|
||
#include "ui_geometry.h"
|
||
#include <algorithm>
|
||
#include <commctrl.h>
|
||
#include <commdlg.h>
|
||
#include <map>
|
||
#include <richedit.h>
|
||
#include <set>
|
||
#include <shellapi.h>
|
||
#include <sstream>
|
||
|
||
namespace ks
|
||
{
|
||
namespace
|
||
{
|
||
constexpr UINT ResultsMessage = WM_APP + 40, PreviewMessage = WM_APP + 41;
|
||
int scale(HWND w, int n)
|
||
{
|
||
return MulDiv(n, (int)GetDpiForWindow(w), 96);
|
||
}
|
||
HFONT font(HWND w)
|
||
{
|
||
return CreateFontW(-scale(w, 14), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
|
||
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH,
|
||
L"Segoe UI");
|
||
}
|
||
struct WindowLayout
|
||
{
|
||
int width{}, height{}, x{}, y{};
|
||
bool scrolling{}, arranging{};
|
||
int wheelRemainder{}, horizontalWheelRemainder{};
|
||
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
||
};
|
||
WindowLayout *windowLayoutState(HWND w)
|
||
{
|
||
return reinterpret_cast<WindowLayout *>(GetPropW(w, L"WindowLayout"));
|
||
}
|
||
RECT workArea(RECT rect)
|
||
{
|
||
MONITORINFO monitor{sizeof(monitor)};
|
||
GetMonitorInfoW(MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST), &monitor);
|
||
return monitor.rcWork;
|
||
}
|
||
void fitToMonitor(HWND w, RECT rect)
|
||
{
|
||
rect = fitWindowRect(rect, workArea(rect));
|
||
SetWindowPos(w, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
|
||
SWP_NOACTIVATE | SWP_NOZORDER);
|
||
}
|
||
void arrangeWindow(HWND w)
|
||
{
|
||
auto *layout = windowLayoutState(w);
|
||
if (!layout || !layout->scrolling || layout->arranging) return;
|
||
layout->arranging = true;
|
||
// Showing either scrollbar can make the other necessary. Let the client size settle.
|
||
for (int pass = 0; pass < 3; ++pass)
|
||
{
|
||
RECT client{};
|
||
GetClientRect(w, &client);
|
||
SCROLLINFO horizontal{sizeof(horizontal), SIF_RANGE | SIF_PAGE | SIF_POS,
|
||
0, scale(w, layout->width) - 1, (UINT)client.right, layout->x};
|
||
SCROLLINFO vertical{sizeof(vertical), SIF_RANGE | SIF_PAGE | SIF_POS,
|
||
0, scale(w, layout->height) - 1, (UINT)client.bottom, layout->y};
|
||
layout->x = SetScrollInfo(w, SB_HORZ, &horizontal, TRUE);
|
||
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
||
}
|
||
for (const auto &[child, r] : layout->controls)
|
||
SetWindowPos(child, nullptr, scale(w, r.left) - layout->x, scale(w, r.top) - layout->y,
|
||
scale(w, r.right - r.left), scale(w, r.bottom - r.top), SWP_NOACTIVATE | SWP_NOZORDER);
|
||
layout->arranging = false;
|
||
InvalidateRect(w, nullptr, TRUE);
|
||
}
|
||
void revealControl(HWND child)
|
||
{
|
||
HWND parent = GetParent(child);
|
||
auto *layout = windowLayoutState(parent);
|
||
if (!layout || !layout->scrolling) return;
|
||
RECT r{}, client{};
|
||
GetWindowRect(child, &r);
|
||
MapWindowPoints(nullptr, parent, reinterpret_cast<POINT *>(&r), 2);
|
||
GetClientRect(parent, &client);
|
||
if (r.left < 0) layout->x += r.left;
|
||
else if (r.right > client.right) layout->x += r.right - client.right;
|
||
if (r.top < 0) layout->y += r.top;
|
||
else if (r.bottom > client.bottom) layout->y += r.bottom - client.bottom;
|
||
arrangeWindow(parent);
|
||
}
|
||
LRESULT CALLBACK scrollChildProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||
{
|
||
if (msg == WM_SETFOCUS) revealControl(w);
|
||
if (msg == WM_NCDESTROY) RemoveWindowSubclass(w, scrollChildProc, 2);
|
||
return DefSubclassProc(w, msg, wp, lp);
|
||
}
|
||
void resizeForDpi(HWND, WPARAM, LPARAM);
|
||
LRESULT CALLBACK layoutProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||
{
|
||
auto *layout = windowLayoutState(w);
|
||
if (!layout) return DefSubclassProc(w, msg, wp, lp);
|
||
if (msg == WM_DPICHANGED)
|
||
{
|
||
resizeForDpi(w, wp, lp);
|
||
return 0;
|
||
}
|
||
if (msg == WM_SIZE) arrangeWindow(w);
|
||
if (msg == WM_DISPLAYCHANGE || msg == WM_SETTINGCHANGE || (msg == WM_SHOWWINDOW && wp))
|
||
{
|
||
RECT rect{};
|
||
GetWindowRect(w, &rect);
|
||
fitToMonitor(w, rect);
|
||
arrangeWindow(w);
|
||
}
|
||
if (layout->scrolling && (msg == WM_VSCROLL || msg == WM_HSCROLL))
|
||
{
|
||
bool vertical = msg == WM_VSCROLL;
|
||
SCROLLINFO info{sizeof(info), SIF_ALL};
|
||
GetScrollInfo(w, vertical ? SB_VERT : SB_HORZ, &info);
|
||
int &position = vertical ? layout->y : layout->x;
|
||
switch (LOWORD(wp))
|
||
{
|
||
case SB_LINEUP: position -= scale(w, 32); break;
|
||
case SB_LINEDOWN: position += scale(w, 32); break;
|
||
case SB_PAGEUP: position -= (int)info.nPage; break;
|
||
case SB_PAGEDOWN: position += (int)info.nPage; break;
|
||
case SB_TOP: position = 0; break;
|
||
case SB_BOTTOM: position = info.nMax; break;
|
||
case SB_THUMBTRACK: case SB_THUMBPOSITION: position = info.nTrackPos; break;
|
||
}
|
||
arrangeWindow(w);
|
||
return 0;
|
||
}
|
||
if (layout->scrolling && (msg == WM_MOUSEWHEEL || msg == WM_MOUSEHWHEEL))
|
||
{
|
||
bool horizontal = msg == WM_MOUSEHWHEEL || (GET_KEYSTATE_WPARAM(wp) & MK_SHIFT);
|
||
int &remainder = horizontal ? layout->horizontalWheelRemainder : layout->wheelRemainder;
|
||
remainder += GET_WHEEL_DELTA_WPARAM(wp);
|
||
int steps = remainder / WHEEL_DELTA;
|
||
remainder %= WHEEL_DELTA;
|
||
(horizontal ? layout->x : layout->y) += steps * scale(w, 48) * (msg == WM_MOUSEHWHEEL ? 1 : -1);
|
||
arrangeWindow(w);
|
||
return 0;
|
||
}
|
||
if (msg == WM_NCDESTROY)
|
||
{
|
||
RemovePropW(w, L"WindowLayout");
|
||
RemoveWindowSubclass(w, layoutProc, 2);
|
||
delete layout;
|
||
}
|
||
return DefSubclassProc(w, msg, wp, lp);
|
||
}
|
||
HWND control(HWND parent, const wchar_t *cls, const wchar_t *title, DWORD style, int id, int x, int y,
|
||
int width, int height)
|
||
{
|
||
HWND w = CreateWindowExW(wcscmp(cls, L"EDIT") == 0 ? WS_EX_CLIENTEDGE : 0, cls, title,
|
||
WS_CHILD | WS_VISIBLE | style, scale(parent, x), scale(parent, y),
|
||
scale(parent, width), scale(parent, height), parent, (HMENU)(INT_PTR)id,
|
||
GetModuleHandleW(nullptr), nullptr);
|
||
SendMessageW(w, WM_SETFONT, (WPARAM)GetPropW(parent, L"Font"), TRUE);
|
||
if (auto *layout = windowLayoutState(parent); layout && layout->scrolling)
|
||
{
|
||
layout->controls.push_back({w, {x, y, x + width, y + height}});
|
||
SetWindowSubclass(w, scrollChildProc, 2, 0);
|
||
arrangeWindow(parent);
|
||
}
|
||
return w;
|
||
}
|
||
void label(HWND w, const wchar_t *text, int x, int y, int width = 200)
|
||
{
|
||
control(w, L"STATIC", text, 0, 0, x, y, width, 22);
|
||
}
|
||
void button(HWND w, const wchar_t *text, int id, int x, int y, int width = 100)
|
||
{
|
||
control(w, L"BUTTON", text, WS_TABSTOP | BS_PUSHBUTTON, id, x, y, width, 28);
|
||
}
|
||
HWND edit(HWND w, int id, int x, int y, int width, int height = 25, DWORD style = ES_AUTOHSCROLL)
|
||
{
|
||
auto c = control(w, L"EDIT", L"", WS_TABSTOP | style, id, x, y, width, height);
|
||
SendMessageW(c, EM_SETLIMITTEXT, 1024 * 1024, 0);
|
||
return c;
|
||
}
|
||
LRESULT CALLBACK hotkeyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR data)
|
||
{
|
||
auto *app = (App *)data;
|
||
if (msg == WM_SETFOCUS)
|
||
{
|
||
app->hotkeyCapture = w;
|
||
SendMessageW(w, EM_SETSEL, 0, -1);
|
||
}
|
||
if (msg == WM_KILLFOCUS || msg == WM_NCDESTROY)
|
||
{
|
||
if (app->hotkeyCapture == w)
|
||
app->hotkeyCapture = nullptr;
|
||
}
|
||
if (msg == CaptureHotkeyMessage)
|
||
{
|
||
UINT mods = (UINT)lp;
|
||
if ((wp == VK_TAB && (mods == 0 || mods == MOD_SHIFT)) || (wp == VK_ESCAPE && mods == 0))
|
||
{
|
||
SetFocus(GetNextDlgTabItem(GetParent(w), w, mods == MOD_SHIFT));
|
||
return 0;
|
||
}
|
||
if ((wp == VK_BACK || wp == VK_DELETE) && mods == 0)
|
||
{
|
||
SetWindowTextW(w, L"");
|
||
return 0;
|
||
}
|
||
auto text = Hotkey{mods, (UINT)wp}.format();
|
||
if (!text.empty())
|
||
{
|
||
SetWindowTextW(w, text.c_str());
|
||
SendMessageW(w, EM_SETSEL, 0, -1);
|
||
}
|
||
return 0;
|
||
}
|
||
if (msg == WM_CHAR || msg == WM_SYSCHAR || msg == WM_PASTE || msg == WM_CUT)
|
||
return 0;
|
||
return DefSubclassProc(w, msg, wp, lp);
|
||
}
|
||
void hotkeyEdit(App &app, HWND w, int id, int x, int y, int width)
|
||
{
|
||
HWND field = edit(w, id, x, y, width, 25, ES_AUTOHSCROLL | ES_READONLY);
|
||
SendMessageW(field, EM_SETCUEBANNER, FALSE, (LPARAM)L"Click, then press a shortcut");
|
||
SetWindowSubclass(field, hotkeyProc, 1, (DWORD_PTR)&app);
|
||
}
|
||
void checkbox(HWND w, const wchar_t *text, int id, int x, int y, int width = 300)
|
||
{
|
||
control(w, L"BUTTON", text, WS_TABSTOP | BS_AUTOCHECKBOX, id, x, y, width, 24);
|
||
}
|
||
std::wstring value(HWND w, int id)
|
||
{
|
||
HWND c = GetDlgItem(w, id);
|
||
int n = GetWindowTextLengthW(c);
|
||
std::wstring text(n + 1, L'\0');
|
||
GetWindowTextW(c, text.data(), n + 1);
|
||
text.resize(n);
|
||
return text;
|
||
}
|
||
void value(HWND w, int id, const std::wstring &text)
|
||
{
|
||
SetDlgItemTextW(w, id, text.c_str());
|
||
}
|
||
bool checked(HWND w, int id)
|
||
{
|
||
return IsDlgButtonChecked(w, id) == BST_CHECKED;
|
||
}
|
||
void check(HWND w, int id, bool yes)
|
||
{
|
||
CheckDlgButton(w, id, yes ? BST_CHECKED : BST_UNCHECKED);
|
||
}
|
||
void error(HWND w, const std::exception &e)
|
||
{
|
||
MessageBoxW(w, wide(e.what()).c_str(), L"KSwitcher", MB_OK | MB_ICONWARNING);
|
||
}
|
||
HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc, int width, int height,
|
||
DWORD extra = 0)
|
||
{
|
||
WNDCLASSW wc{};
|
||
wc.hInstance = app.instance;
|
||
wc.lpfnWndProc = proc;
|
||
wc.lpszClassName = cls;
|
||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
|
||
wc.hIcon = LoadIconW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER));
|
||
RegisterClassW(&wc);
|
||
UINT dpi = GetDpiForSystem();
|
||
RECT rect{0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
|
||
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_THICKFRAME | WS_MAXIMIZEBOX | extra;
|
||
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
|
||
POINT cursor{};
|
||
GetCursorPos(&cursor);
|
||
MONITORINFO monitor{sizeof(monitor)};
|
||
GetMonitorInfoW(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &monitor);
|
||
HWND w =
|
||
CreateWindowExW(WS_EX_APPWINDOW, cls, title, style, monitor.rcWork.left, monitor.rcWork.top,
|
||
rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, app.instance, &app);
|
||
if (!w)
|
||
throw std::runtime_error(utf8(lastError(L"Cannot create native window")));
|
||
// The system DPI may still describe the old primary display. The HWND tells
|
||
// us the actual destination DPI, and must size the frame AND its controls.
|
||
dpi = GetDpiForWindow(w);
|
||
rect = {0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
|
||
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
|
||
int outerWidth = rect.right - rect.left, outerHeight = rect.bottom - rect.top;
|
||
int left = monitor.rcWork.left + std::max(0L, (monitor.rcWork.right - monitor.rcWork.left - outerWidth) / 2);
|
||
int top = monitor.rcWork.top + std::max(0L, (monitor.rcWork.bottom - monitor.rcWork.top - outerHeight) / 2);
|
||
SetPropW(w, L"WindowLayout", new WindowLayout{width, height, 0, 0, !(extra & WS_THICKFRAME)});
|
||
SetWindowSubclass(w, layoutProc, 2, 0);
|
||
RECT initial = fitWindowRect({left, top, left + outerWidth, top + outerHeight}, monitor.rcWork);
|
||
SetWindowPos(w, nullptr, initial.left, initial.top, initial.right - initial.left, initial.bottom - initial.top,
|
||
SWP_NOACTIVATE | SWP_NOZORDER);
|
||
arrangeWindow(w);
|
||
SetWindowTextW(w, title);
|
||
auto smallIcon = (HICON)LoadImageW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
|
||
GetSystemMetricsForDpi(SM_CXSMICON, dpi),
|
||
GetSystemMetricsForDpi(SM_CYSMICON, dpi), LR_SHARED);
|
||
SendMessageW(w, WM_SETICON, ICON_SMALL, (LPARAM)smallIcon);
|
||
SetPropW(w, L"Font", font(w));
|
||
SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)GetDpiForWindow(w));
|
||
return w;
|
||
}
|
||
void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||
{
|
||
// Creation may send WM_DPICHANGED before controls and the original font exist.
|
||
if (!GetPropW(w, L"Dpi"))
|
||
{
|
||
fitToMonitor(w, *(RECT *)lp);
|
||
return;
|
||
}
|
||
int previous = (int)(UINT_PTR)GetPropW(w, L"Dpi");
|
||
int next = LOWORD(wp);
|
||
if (!previous)
|
||
previous = 96;
|
||
HFONT old = (HFONT)GetPropW(w, L"Font");
|
||
HFONT replacement = font(w);
|
||
struct State
|
||
{
|
||
HWND parent;
|
||
int previous, next;
|
||
HFONT font;
|
||
} state{w, previous, next, replacement};
|
||
auto *layout = windowLayoutState(w);
|
||
if (!layout || !layout->scrolling) EnumChildWindows(
|
||
w,
|
||
[](HWND child, LPARAM data) -> BOOL
|
||
{
|
||
auto &s = *(State *)data;
|
||
if (GetParent(child) != s.parent)
|
||
return TRUE;
|
||
RECT r;
|
||
GetWindowRect(child, &r);
|
||
MapWindowPoints(nullptr, s.parent, (POINT *)&r, 2);
|
||
SetWindowPos(child, nullptr, MulDiv(r.left, s.next, s.previous),
|
||
MulDiv(r.top, s.next, s.previous), MulDiv(r.right - r.left, s.next, s.previous),
|
||
MulDiv(r.bottom - r.top, s.next, s.previous), SWP_NOZORDER);
|
||
SendMessageW(child, WM_SETFONT, (WPARAM)s.font, TRUE);
|
||
return TRUE;
|
||
},
|
||
(LPARAM)&state);
|
||
else
|
||
{
|
||
layout->x = MulDiv(layout->x, next, previous);
|
||
layout->y = MulDiv(layout->y, next, previous);
|
||
for (auto &[child, rect] : layout->controls)
|
||
SendMessageW(child, WM_SETFONT, (WPARAM)replacement, TRUE);
|
||
}
|
||
SetPropW(w, L"Font", replacement);
|
||
SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)next);
|
||
if (old)
|
||
DeleteObject(old);
|
||
fitToMonitor(w, *(RECT *)lp);
|
||
arrangeWindow(w);
|
||
}
|
||
std::filesystem::path pickFile(HWND owner, bool save)
|
||
{
|
||
wchar_t path[32768]{};
|
||
OPENFILENAMEW ofn{sizeof(ofn)};
|
||
ofn.hwndOwner = owner;
|
||
ofn.lpstrFilter = L"Settings (*.json)\0*.json\0All files\0*.*\0";
|
||
ofn.lpstrFile = path;
|
||
ofn.nMaxFile = 32768;
|
||
ofn.lpstrDefExt = L"json";
|
||
ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR | (save ? OFN_OVERWRITEPROMPT : OFN_FILEMUSTEXIST);
|
||
return (save ? GetSaveFileNameW(&ofn) : GetOpenFileNameW(&ofn)) ? std::filesystem::path(path)
|
||
: std::filesystem::path();
|
||
}
|
||
void disposeFont(HWND w)
|
||
{
|
||
auto h = (HFONT)RemovePropW(w, L"Font");
|
||
if (h)
|
||
DeleteObject(h);
|
||
}
|
||
|
||
struct SearchResult
|
||
{
|
||
uint64_t generation;
|
||
std::vector<Entry> items;
|
||
};
|
||
struct PreviewResult
|
||
{
|
||
uint64_t generation;
|
||
std::wstring id;
|
||
Pixels pixels;
|
||
std::string rtf;
|
||
};
|
||
struct HistoryView
|
||
{
|
||
App *app{};
|
||
std::vector<Entry> items;
|
||
uint64_t generation{};
|
||
std::map<std::wstring, Pixels> thumbnails;
|
||
std::map<std::wstring, std::string> richTexts;
|
||
HWND rich{};
|
||
std::set<std::wstring> pending;
|
||
};
|
||
std::wstring selected(HWND w, HistoryView *v)
|
||
{
|
||
int i = (int)SendDlgItemMessageW(w, 102, LB_GETCURSEL, 0, 0);
|
||
return i >= 0 && i < (int)v->items.size() ? v->items[i].id : L"";
|
||
}
|
||
void search(HWND w, HistoryView *v)
|
||
{
|
||
auto q = value(w, 101);
|
||
auto generation = ++v->generation;
|
||
v->pending.clear();
|
||
auto *app = v->app;
|
||
app->enqueue(
|
||
[w, app, q, generation]
|
||
{
|
||
try
|
||
{
|
||
auto result = new SearchResult{generation, app->history->search(q)};
|
||
if (!PostMessageW(w, ResultsMessage, 0, (LPARAM)result))
|
||
delete result;
|
||
}
|
||
catch (const std::exception &e)
|
||
{
|
||
app->report(wide(e.what()));
|
||
}
|
||
});
|
||
}
|
||
void requestPreview(HWND w, HistoryView *v, const Entry &entry)
|
||
{
|
||
const auto id = entry.id;
|
||
const bool image = entry.image;
|
||
if (v->thumbnails.contains(id) || v->richTexts.contains(id) || !v->pending.insert(id).second)
|
||
return;
|
||
auto generation = v->generation;
|
||
auto *app = v->app;
|
||
app->enqueue(
|
||
[w, app, id, generation, image]
|
||
{
|
||
auto result = new PreviewResult{generation, id, {}, {}};
|
||
try
|
||
{
|
||
auto clip = app->history->load(id);
|
||
if (image)
|
||
result->pixels = decodeImage(clip, 208, 128);
|
||
else
|
||
result->rtf = richPreview(clip);
|
||
}
|
||
catch (const std::exception &)
|
||
{
|
||
}
|
||
if (!PostMessageW(w, PreviewMessage, 0, (LPARAM)result))
|
||
delete result;
|
||
});
|
||
}
|
||
void drawHistoryRow(HWND w, HistoryView *v, const DRAWITEMSTRUCT &draw)
|
||
{
|
||
if (draw.itemID >= v->items.size())
|
||
return;
|
||
const auto &entry = v->items[draw.itemID];
|
||
const bool selected = (draw.itemState & ODS_SELECTED) != 0;
|
||
HDC dc = draw.hDC;
|
||
int saved = SaveDC(dc);
|
||
auto box = draw.rcItem;
|
||
FillRect(dc, &box, GetSysColorBrush(selected ? COLOR_HIGHLIGHT : COLOR_WINDOW));
|
||
SetBkMode(dc, TRANSPARENT);
|
||
SetTextColor(dc, GetSysColor(selected ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT));
|
||
SelectObject(dc, GetPropW(w, L"Font"));
|
||
RECT icon{box.left + scale(w, 12), box.top + scale(w, 10), box.left + scale(w, 116),
|
||
box.bottom - scale(w, 10)};
|
||
auto thumbnail = v->thumbnails.find(entry.id);
|
||
if (entry.image && thumbnail != v->thumbnails.end() && !thumbnail->second.bgra.empty())
|
||
{
|
||
auto &p = thumbnail->second;
|
||
double ratio =
|
||
std::min(double(icon.right - icon.left) / p.width, double(icon.bottom - icon.top) / p.height);
|
||
int width = int(p.width * ratio), height = int(p.height * ratio);
|
||
BITMAPINFO info{};
|
||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||
info.bmiHeader.biWidth = p.width;
|
||
info.bmiHeader.biHeight = -(LONG)p.height;
|
||
info.bmiHeader.biPlanes = 1;
|
||
info.bmiHeader.biBitCount = 32;
|
||
SetStretchBltMode(dc, HALFTONE);
|
||
StretchDIBits(dc, icon.left + (icon.right - icon.left - width) / 2,
|
||
icon.top + (icon.bottom - icon.top - height) / 2, width, height, 0, 0, p.width,
|
||
p.height, p.bgra.data(), &info, DIB_RGB_COLORS, SRCCOPY);
|
||
}
|
||
else
|
||
{
|
||
FrameRect(dc, &icon, GetSysColorBrush(selected ? COLOR_HIGHLIGHTTEXT : COLOR_3DSHADOW));
|
||
const wchar_t *glyph = entry.image ? L"▧" : entry.files ? L"≡" : L"Aa";
|
||
DrawTextW(dc, glyph, -1, &icon, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||
if (entry.image)
|
||
requestPreview(w, v, entry);
|
||
}
|
||
RECT title{icon.right + scale(w, 14), box.top + scale(w, 10), box.right - scale(w, 14),
|
||
box.bottom - scale(w, 30)};
|
||
auto text = (entry.pinned ? L"★ " : L"") + (entry.image ? L"Image"
|
||
: entry.files ? L"Files"
|
||
: entry.preview);
|
||
bool formatted = false;
|
||
if (!entry.image && !entry.files)
|
||
{
|
||
auto rich = v->richTexts.find(entry.id);
|
||
if (rich != v->richTexts.end() && v->rich && !rich->second.empty())
|
||
{
|
||
setRichText(v->rich, rich->second);
|
||
CHARFORMAT2W format{};
|
||
format.cbSize = sizeof(format);
|
||
format.dwMask = CFM_COLOR;
|
||
format.crTextColor = GetSysColor(selected ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT);
|
||
SendMessageW(v->rich, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&format);
|
||
SendMessageW(v->rich, EM_SETBKGNDCOLOR, 0,
|
||
GetSysColor(selected ? COLOR_HIGHLIGHT : COLOR_WINDOW));
|
||
FORMATRANGE range{};
|
||
range.hdc = range.hdcTarget = dc;
|
||
range.rc = {MulDiv(title.left, 1440, GetDeviceCaps(dc, LOGPIXELSX)),
|
||
MulDiv(title.top, 1440, GetDeviceCaps(dc, LOGPIXELSY)),
|
||
MulDiv(title.right, 1440, GetDeviceCaps(dc, LOGPIXELSX)),
|
||
MulDiv(title.bottom, 1440, GetDeviceCaps(dc, LOGPIXELSY))};
|
||
range.rcPage = range.rc;
|
||
range.chrg = {0, -1};
|
||
IntersectClipRect(dc, title.left, title.top, title.right, title.bottom);
|
||
SendMessageW(v->rich, EM_FORMATRANGE, TRUE, (LPARAM)&range);
|
||
SendMessageW(v->rich, EM_FORMATRANGE, FALSE, 0);
|
||
// Restore the row clipping before drawing its source label.
|
||
RestoreDC(dc, saved);
|
||
saved = SaveDC(dc);
|
||
SetBkMode(dc, TRANSPARENT);
|
||
SelectObject(dc, GetPropW(w, L"Font"));
|
||
formatted = true;
|
||
}
|
||
else
|
||
requestPreview(w, v, entry);
|
||
}
|
||
if (!formatted)
|
||
DrawTextW(dc, text.c_str(), -1, &title, DT_WORDBREAK | DT_END_ELLIPSIS | DT_NOPREFIX);
|
||
RECT detail{title.left, box.bottom - scale(w, 26), title.right, box.bottom - scale(w, 8)};
|
||
SetTextColor(dc, GetSysColor(selected ? COLOR_HIGHLIGHTTEXT : COLOR_GRAYTEXT));
|
||
auto subtitle = (entry.pinned ? L"★ " : L"") + std::to_wstring(draw.itemID + 1) + L" · " + entry.app;
|
||
DrawTextW(dc, subtitle.c_str(), -1, &detail, DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX);
|
||
if (draw.itemState & ODS_FOCUS)
|
||
{
|
||
InflateRect(&box, -2, -2);
|
||
DrawFocusRect(dc, &box);
|
||
}
|
||
RestoreDC(dc, saved);
|
||
}
|
||
LRESULT CALLBACK historyKey(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR data)
|
||
{
|
||
HWND parent = (HWND)data;
|
||
if (msg == WM_KEYDOWN)
|
||
{
|
||
if (wp == VK_ESCAPE)
|
||
{
|
||
SendMessageW(parent, WM_CLOSE, 0, 0);
|
||
return 0;
|
||
}
|
||
if (wp == VK_RETURN)
|
||
{
|
||
SendMessageW(parent, WM_COMMAND, (GetKeyState(VK_SHIFT) & 0x8000) ? 111 : 110, 0);
|
||
return 0;
|
||
}
|
||
if (GetKeyState(VK_CONTROL) & 0x8000)
|
||
{
|
||
if (wp >= L'1' && wp <= L'9')
|
||
{
|
||
SendDlgItemMessageW(parent, 102, LB_SETCURSEL, wp - L'1', 0);
|
||
SendMessageW(parent, WM_COMMAND, 110, 0);
|
||
return 0;
|
||
}
|
||
if (wp == L'P')
|
||
{
|
||
SendMessageW(parent, WM_COMMAND, 112, 0);
|
||
return 0;
|
||
}
|
||
if (wp == L'O')
|
||
{
|
||
SendMessageW(parent, WM_COMMAND, 114, 0);
|
||
return 0;
|
||
}
|
||
}
|
||
if (GetDlgCtrlID(w) == 102 && wp == VK_DELETE)
|
||
{
|
||
SendMessageW(parent, WM_COMMAND, 113, 0);
|
||
return 0;
|
||
}
|
||
if (GetDlgCtrlID(w) == 101 && wp == VK_DOWN)
|
||
{
|
||
SetFocus(GetDlgItem(parent, 102));
|
||
return 0;
|
||
}
|
||
}
|
||
return DefSubclassProc(w, msg, wp, lp);
|
||
}
|
||
LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||
{
|
||
auto *v = (HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
||
if (msg == WM_NCCREATE)
|
||
{
|
||
auto *app = (App *)((CREATESTRUCTW *)lp)->lpCreateParams;
|
||
v = new HistoryView;
|
||
v->app = app;
|
||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v);
|
||
return TRUE;
|
||
}
|
||
if (!v)
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
try
|
||
{
|
||
switch (msg)
|
||
{
|
||
case WM_COMMAND:
|
||
{
|
||
int id = LOWORD(wp);
|
||
if (id == 101 && HIWORD(wp) == EN_CHANGE)
|
||
{
|
||
SetTimer(w, 1, 180, nullptr);
|
||
return 0;
|
||
}
|
||
if (id == 102 && HIWORD(wp) == LBN_SELCHANGE)
|
||
{
|
||
return 0;
|
||
}
|
||
if (id == 102 && HIWORD(wp) == LBN_DBLCLK)
|
||
id = 110;
|
||
auto entry = selected(w, v);
|
||
if (id == 110 || id == 111)
|
||
{
|
||
if (!entry.empty())
|
||
v->app->pasteEntry(entry, id == 111);
|
||
return 0;
|
||
}
|
||
if (id == 112 || id == 113)
|
||
{
|
||
if (!entry.empty())
|
||
{
|
||
if (id == 112)
|
||
v->app->history->pin(entry);
|
||
else
|
||
v->app->history->remove(entry);
|
||
search(w, v);
|
||
}
|
||
return 0;
|
||
}
|
||
if (id == 115 && !entry.empty())
|
||
{
|
||
v->app->recognizeEntry(entry);
|
||
return 0;
|
||
}
|
||
if (id == 114 && !entry.empty())
|
||
{
|
||
showEntry(*v->app, entry);
|
||
return 0;
|
||
}
|
||
return 0;
|
||
}
|
||
case WM_TIMER:
|
||
KillTimer(w, 1);
|
||
search(w, v);
|
||
return 0;
|
||
case ResultsMessage:
|
||
{
|
||
std::unique_ptr<SearchResult> result((SearchResult *)lp);
|
||
if (result->generation != v->generation)
|
||
return 0;
|
||
auto old = selected(w, v);
|
||
v->items = std::move(result->items);
|
||
SendDlgItemMessageW(w, 102, WM_SETREDRAW, FALSE, 0);
|
||
SendDlgItemMessageW(w, 102, LB_RESETCONTENT, 0, 0);
|
||
int select = 0;
|
||
for (size_t i = 0; i < v->items.size(); ++i)
|
||
{
|
||
auto &e = v->items[i];
|
||
auto text = (e.pinned ? L"★ " : L"") + e.preview + L" — " + e.app;
|
||
SendDlgItemMessageW(w, 102, LB_ADDSTRING, 0, (LPARAM)text.c_str());
|
||
if (e.id == old)
|
||
select = (int)i;
|
||
}
|
||
SendDlgItemMessageW(w, 102, LB_SETCURSEL, select, 0);
|
||
SendDlgItemMessageW(w, 102, WM_SETREDRAW, TRUE, 0);
|
||
InvalidateRect(GetDlgItem(w, 102), nullptr, TRUE);
|
||
return 0;
|
||
}
|
||
case PreviewMessage:
|
||
{
|
||
std::unique_ptr<PreviewResult> result((PreviewResult *)lp);
|
||
if (result->generation != v->generation || !IsWindowVisible(w))
|
||
return 0;
|
||
v->pending.erase(result->id);
|
||
// At most ~3.3 MiB, irrespective of history length or source image size.
|
||
if (v->thumbnails.size() >= 32)
|
||
v->thumbnails.erase(v->thumbnails.begin());
|
||
if (v->richTexts.size() >= 32)
|
||
v->richTexts.erase(v->richTexts.begin());
|
||
if (!result->rtf.empty())
|
||
v->richTexts[result->id] = std::move(result->rtf);
|
||
else
|
||
v->thumbnails[result->id] = std::move(result->pixels);
|
||
InvalidateRect(GetDlgItem(w, 102), nullptr, FALSE);
|
||
return 0;
|
||
}
|
||
case WM_MEASUREITEM:
|
||
if (((MEASUREITEMSTRUCT *)lp)->CtlID == 102)
|
||
{
|
||
((MEASUREITEMSTRUCT *)lp)->itemHeight = scale(w, 88);
|
||
return TRUE;
|
||
}
|
||
break;
|
||
case WM_DRAWITEM:
|
||
if (((DRAWITEMSTRUCT *)lp)->CtlID == 102)
|
||
{
|
||
drawHistoryRow(w, v, *(DRAWITEMSTRUCT *)lp);
|
||
return TRUE;
|
||
}
|
||
break;
|
||
case WM_CONTEXTMENU:
|
||
{
|
||
HMENU menu = CreatePopupMenu();
|
||
AppendMenuW(menu, MF_STRING, 110, L"Paste");
|
||
AppendMenuW(menu, MF_STRING, 111, L"Paste plain text");
|
||
AppendMenuW(menu, MF_STRING, 112, L"Pin / unpin");
|
||
AppendMenuW(menu, MF_STRING, 113, L"Delete");
|
||
AppendMenuW(menu, MF_STRING, 114, L"Open full item");
|
||
AppendMenuW(menu, MF_STRING, 115, L"Copy text from image (Windows OCR)");
|
||
POINT p;
|
||
GetCursorPos(&p);
|
||
int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, p.x, p.y, 0, w, nullptr);
|
||
DestroyMenu(menu);
|
||
if (cmd)
|
||
SendMessageW(w, WM_COMMAND, cmd, 0);
|
||
return 0;
|
||
}
|
||
case WM_CLOSE:
|
||
ShowWindow(w, SW_HIDE);
|
||
v->thumbnails.clear();
|
||
v->richTexts.clear();
|
||
if (v->rich)
|
||
SetWindowTextW(v->rich, L"");
|
||
v->pending.clear();
|
||
++v->generation;
|
||
return 0;
|
||
case WM_DPICHANGED:
|
||
{
|
||
resizeForDpi(w, wp, lp);
|
||
SendDlgItemMessageW(w, 102, LB_SETITEMHEIGHT, 0, scale(w, 88));
|
||
return 0;
|
||
}
|
||
case WM_NCDESTROY:
|
||
disposeFont(w);
|
||
v->app->historyWindow = nullptr;
|
||
delete v;
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
}
|
||
catch (const std::exception &e)
|
||
{
|
||
error(w, e);
|
||
}
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
|
||
struct SettingsView
|
||
{
|
||
App *app{};
|
||
Settings draft;
|
||
std::vector<int> order;
|
||
int selection = -1;
|
||
bool filling{};
|
||
};
|
||
void collectLayout(HWND w, SettingsView *v)
|
||
{
|
||
if (v->selection < 0 || v->selection >= (int)v->order.size())
|
||
return;
|
||
int index = v->order[v->selection];
|
||
auto id = v->app->layouts[index].id;
|
||
auto &cycle = v->draft.cycle;
|
||
std::erase(cycle, id);
|
||
if (checked(w, 230))
|
||
cycle.push_back(id);
|
||
if (v->draft.direct.size() < v->app->layouts.size())
|
||
v->draft.direct.resize(v->app->layouts.size());
|
||
v->draft.direct[index] = value(w, 231);
|
||
}
|
||
void layoutSelection(HWND w, SettingsView *v)
|
||
{
|
||
v->selection = (int)SendDlgItemMessageW(w, 220, LB_GETCURSEL, 0, 0);
|
||
if (v->selection < 0)
|
||
return;
|
||
int i = v->order[v->selection];
|
||
auto &l = v->app->layouts[i];
|
||
check(w, 230, std::find(v->draft.cycle.begin(), v->draft.cycle.end(), l.id) != v->draft.cycle.end());
|
||
EnableWindow(GetDlgItem(w, 230), l.convertible);
|
||
value(w, 231, i < (int)v->draft.direct.size() ? v->draft.direct[i] : L"");
|
||
}
|
||
void fillLayouts(HWND w, SettingsView *v)
|
||
{
|
||
SendDlgItemMessageW(w, 220, LB_RESETCONTENT, 0, 0);
|
||
for (int i : v->order)
|
||
{
|
||
auto &l = v->app->layouts[i];
|
||
bool on = std::find(v->draft.cycle.begin(), v->draft.cycle.end(), l.id) != v->draft.cycle.end();
|
||
auto title = (on ? L"[✓] " : L"[ ] ") + l.name + (l.convertible ? L"" : L" — switch only");
|
||
SendDlgItemMessageW(w, 220, LB_ADDSTRING, 0, (LPARAM)title.c_str());
|
||
}
|
||
SendDlgItemMessageW(w, 220, LB_SETCURSEL, std::max(0, v->selection), 0);
|
||
layoutSelection(w, v);
|
||
}
|
||
void fillSettings(HWND w, SettingsView *v)
|
||
{
|
||
v->filling = true;
|
||
auto &s = v->draft;
|
||
check(w, 201, s.recent);
|
||
check(w, 202, s.switchLayout);
|
||
check(w, 208, s.writeLog);
|
||
check(w, 203, s.history);
|
||
check(w, 204, s.autostart);
|
||
check(w, 206, s.elevatedAutostart);
|
||
check(w, 205, s.awake);
|
||
check(w, 207, s.autoCorrect);
|
||
value(w, 210, s.toggle);
|
||
value(w, 211, s.casing);
|
||
value(w, 212, s.translit);
|
||
value(w, 213, s.undo);
|
||
value(w, 214, s.clipboard);
|
||
value(w, 240, std::to_wstring(s.maxItems));
|
||
value(w, 241, std::to_wstring(s.maxMiB));
|
||
value(w, 242, std::to_wstring(s.maxImageMiB));
|
||
std::wstring excluded;
|
||
for (auto &name : s.excluded)
|
||
{
|
||
if (!excluded.empty())
|
||
excluded += L", ";
|
||
excluded += name;
|
||
}
|
||
value(w, 243, excluded);
|
||
v->order.clear();
|
||
for (auto &id : s.cycle)
|
||
for (int i = 0; i < (int)v->app->layouts.size(); ++i)
|
||
if (v->app->layouts[i].id == id)
|
||
v->order.push_back(i);
|
||
for (int i = 0; i < (int)v->app->layouts.size(); ++i)
|
||
if (std::find(v->order.begin(), v->order.end(), i) == v->order.end())
|
||
v->order.push_back(i);
|
||
v->selection = 0;
|
||
fillLayouts(w, v);
|
||
v->filling = false;
|
||
}
|
||
int integer(HWND w, int id)
|
||
{
|
||
auto s = value(w, id);
|
||
size_t n{};
|
||
int v = std::stoi(s, &n);
|
||
if (n != s.size())
|
||
throw std::runtime_error("Enter a whole number for history limits");
|
||
return v;
|
||
}
|
||
Settings collectSettings(HWND w, SettingsView *v)
|
||
{
|
||
collectLayout(w, v);
|
||
auto s = v->draft;
|
||
s.recent = checked(w, 201);
|
||
s.switchLayout = checked(w, 202);
|
||
s.writeLog = checked(w, 208);
|
||
s.history = checked(w, 203);
|
||
s.autostart = checked(w, 204);
|
||
s.elevatedAutostart = checked(w, 206);
|
||
s.awake = checked(w, 205);
|
||
s.autoCorrect = checked(w, 207);
|
||
s.toggle = value(w, 210);
|
||
s.casing = value(w, 211);
|
||
s.translit = value(w, 212);
|
||
s.undo = value(w, 213);
|
||
s.clipboard = value(w, 214);
|
||
s.maxItems = integer(w, 240);
|
||
s.maxMiB = integer(w, 241);
|
||
s.maxImageMiB = integer(w, 242);
|
||
s.excluded.clear();
|
||
std::wistringstream input(value(w, 243));
|
||
std::wstring item;
|
||
while (std::getline(input, item, L','))
|
||
{
|
||
auto first = item.find_first_not_of(L" \t"), last = item.find_last_not_of(L" \t");
|
||
if (first != item.npos)
|
||
s.excluded.push_back(item.substr(first, last - first + 1));
|
||
}
|
||
std::vector<std::wstring> cycle;
|
||
for (int i : v->order)
|
||
{
|
||
auto id = v->app->layouts[i].id;
|
||
if (std::find(s.cycle.begin(), s.cycle.end(), id) != s.cycle.end())
|
||
cycle.push_back(id);
|
||
}
|
||
s.cycle = cycle;
|
||
validateSettings(s);
|
||
return s;
|
||
}
|
||
struct AutoOptionsView
|
||
{
|
||
HWND parent;
|
||
SettingsView *settings;
|
||
};
|
||
std::wstring commaList(const std::vector<std::wstring> &items)
|
||
{
|
||
std::wstring text;
|
||
for (auto &item : items)
|
||
{
|
||
if (!text.empty())
|
||
text += L", ";
|
||
text += item;
|
||
}
|
||
return text;
|
||
}
|
||
std::vector<std::wstring> parseCommaList(const std::wstring &text)
|
||
{
|
||
std::vector<std::wstring> items;
|
||
std::wistringstream input(text);
|
||
std::wstring item;
|
||
while (std::getline(input, item, L','))
|
||
{
|
||
auto first = item.find_first_not_of(L" \t\r\n"), last = item.find_last_not_of(L" \t\r\n");
|
||
if (first != item.npos)
|
||
items.push_back(item.substr(first, last - first + 1));
|
||
}
|
||
return items;
|
||
}
|
||
LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||
{
|
||
auto *view = (AutoOptionsView *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
||
if (!view)
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
if (msg == WM_COMMAND && LOWORD(wp) == 407)
|
||
{
|
||
auto folder = std::filesystem::path(executable()).parent_path() / L"dictionaries";
|
||
if ((INT_PTR)ShellExecuteW(w, L"open", folder.c_str(), nullptr, nullptr, SW_SHOWNORMAL) <= 32)
|
||
MessageBoxW(w, L"Could not open the dictionary folder.", L"KSwitcher", MB_OK | MB_ICONERROR);
|
||
return 0;
|
||
}
|
||
if (msg == WM_COMMAND && LOWORD(wp) == IDOK)
|
||
{
|
||
try
|
||
{
|
||
auto draft = view->settings->draft;
|
||
auto lengthText = value(w, 410);
|
||
if (lengthText.empty() || lengthText.size() > 2 ||
|
||
lengthText.find_first_not_of(L"0123456789") != lengthText.npos)
|
||
throw std::runtime_error("Minimum word length must be between 1 and 32 letters");
|
||
draft.autoMinLength = std::stoi(lengthText);
|
||
auto rankText = value(w, 406);
|
||
if (rankText.empty() || rankText.find_first_not_of(L"0123456789") != rankText.npos)
|
||
throw std::runtime_error("Enter a non-negative whole number for the frequency rank limit");
|
||
try
|
||
{
|
||
draft.autoMaxRank = std::stoi(rankText);
|
||
}
|
||
catch (...)
|
||
{
|
||
throw std::runtime_error("Frequency rank limit is too large");
|
||
}
|
||
draft.autoLanguages.clear();
|
||
if (checked(w, 401))
|
||
draft.autoLanguages.push_back(L"en");
|
||
if (checked(w, 402))
|
||
draft.autoLanguages.push_back(L"uk");
|
||
if (checked(w, 403))
|
||
draft.autoLanguages.push_back(L"ru");
|
||
draft.autoNever = parseCommaList(value(w, 405));
|
||
draft.autoExcluded = parseCommaList(value(w, 404));
|
||
draft.autoPrefixChars = value(w, 408);
|
||
draft.autoSuffixChars = value(w, 409);
|
||
if (draft.autoLanguages.size() < 2)
|
||
throw std::runtime_error("Select at least two languages");
|
||
validateSettings(draft);
|
||
view->settings->draft.autoLanguages = draft.autoLanguages;
|
||
view->settings->draft.autoNever = draft.autoNever;
|
||
view->settings->draft.autoExcluded = draft.autoExcluded;
|
||
view->settings->draft.autoMaxRank = draft.autoMaxRank;
|
||
view->settings->draft.autoMinLength = draft.autoMinLength;
|
||
view->settings->draft.autoPrefixChars = draft.autoPrefixChars;
|
||
view->settings->draft.autoSuffixChars = draft.autoSuffixChars;
|
||
DestroyWindow(w);
|
||
}
|
||
catch (const std::exception &e)
|
||
{
|
||
error(w, e);
|
||
}
|
||
return 0;
|
||
}
|
||
if ((msg == WM_COMMAND && LOWORD(wp) == IDCANCEL) || msg == WM_CLOSE)
|
||
{
|
||
DestroyWindow(w);
|
||
return 0;
|
||
}
|
||
if (msg == WM_DPICHANGED)
|
||
{
|
||
resizeForDpi(w, wp, lp);
|
||
return 0;
|
||
}
|
||
if (msg == WM_NCDESTROY)
|
||
{
|
||
EnableWindow(view->parent, TRUE);
|
||
SetForegroundWindow(view->parent);
|
||
disposeFont(w);
|
||
delete view;
|
||
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
|
||
}
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
void showAutoOptions(HWND parent, SettingsView *v)
|
||
{
|
||
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc,
|
||
560, 680);
|
||
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v});
|
||
label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525);
|
||
checkbox(w, L"English", 401, 18, 48, 150);
|
||
checkbox(w, L"Ukrainian", 402, 188, 48, 150);
|
||
checkbox(w, L"Russian", 403, 358, 48, 150);
|
||
for (auto &lang : v->draft.autoLanguages)
|
||
check(w, lang == L"en" ? 401 : lang == L"uk" ? 402 : 403, true);
|
||
label(w, L"Excluded applications (process names, comma-separated)", 18, 94, 525);
|
||
edit(w, 404, 18, 122, 524, 84, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL);
|
||
value(w, 404, commaList(v->draft.autoExcluded));
|
||
label(w, L"Personal dictionary: never correct these words (comma-separated)", 18, 220, 525);
|
||
edit(w, 405, 18, 248, 524, 84, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL);
|
||
value(w, 405, commaList(v->draft.autoNever));
|
||
label(w, L"Corrects after Space and a short pause.", 18, 344, 525);
|
||
label(w, L"Undo skips this word until KSwitcher restarts.", 18, 366, 525);
|
||
label(w, L"Frequency rank limit (0 = entire dictionary)", 18, 400, 390);
|
||
edit(w, 406, 420, 396, 122, 26, ES_NUMBER);
|
||
value(w, 406, std::to_wstring(v->draft.autoMaxRank));
|
||
label(w, L"Minimum word length (1–32 letters)", 18, 434, 390);
|
||
edit(w, 410, 420, 430, 122, 26, ES_NUMBER);
|
||
value(w, 410, std::to_wstring(v->draft.autoMinLength));
|
||
label(w, L"Characters before a word", 18, 474, 250);
|
||
label(w, L"Characters after a word", 290, 474, 250);
|
||
edit(w, 408, 18, 500, 252);
|
||
edit(w, 409, 290, 500, 252);
|
||
value(w, 408, v->draft.autoPrefixChars);
|
||
value(w, 409, v->draft.autoSuffixChars);
|
||
label(w, L"Enter punctuation without spaces or separators. These characters are kept.", 18, 530, 525);
|
||
button(w, L"Open dictionary folder", 407, 18, 562, 220);
|
||
label(w, L"After editing files, click OK here, then Save in Settings to reload.", 18, 600, 525);
|
||
button(w, L"OK", IDOK, 326, 632, 100);
|
||
button(w, L"Cancel", IDCANCEL, 440, 632, 100);
|
||
EnableWindow(parent, FALSE);
|
||
showRequestedWindow(w);
|
||
SetForegroundWindow(w);
|
||
}
|
||
LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||
{
|
||
if (msg == WM_DPICHANGED)
|
||
{
|
||
resizeForDpi(w, wp, lp);
|
||
return 0;
|
||
}
|
||
auto *v = (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
||
if (msg == WM_NCCREATE)
|
||
{
|
||
auto *app = (App *)((CREATESTRUCTW *)lp)->lpCreateParams;
|
||
v = new SettingsView{app, app->settings};
|
||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v);
|
||
return TRUE;
|
||
}
|
||
if (!v)
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
try
|
||
{
|
||
switch (msg)
|
||
{
|
||
case WM_COMMAND:
|
||
{
|
||
int id = LOWORD(wp);
|
||
if (id == 220 && HIWORD(wp) == LBN_SELCHANGE && !v->filling)
|
||
{
|
||
collectLayout(w, v);
|
||
layoutSelection(w, v);
|
||
return 0;
|
||
}
|
||
if (id == 256)
|
||
{
|
||
showLogs(*v->app);
|
||
return 0;
|
||
}
|
||
if (id == 230)
|
||
{
|
||
collectLayout(w, v);
|
||
fillLayouts(w, v);
|
||
return 0;
|
||
}
|
||
if (id == 232 || id == 233)
|
||
{
|
||
collectLayout(w, v);
|
||
int next = v->selection + (id == 232 ? -1 : 1);
|
||
if (next >= 0 && next < (int)v->order.size())
|
||
{
|
||
std::swap(v->order[next], v->order[v->selection]);
|
||
v->selection = next;
|
||
fillLayouts(w, v);
|
||
}
|
||
return 0;
|
||
}
|
||
if (id == IDOK)
|
||
{
|
||
if (v->app->busy)
|
||
throw std::runtime_error("Wait for the current text operation to finish");
|
||
v->app->applySettings(collectSettings(w, v));
|
||
DestroyWindow(w);
|
||
return 0;
|
||
}
|
||
if (id == IDCANCEL)
|
||
{
|
||
DestroyWindow(w);
|
||
return 0;
|
||
}
|
||
if (id == 250)
|
||
{
|
||
auto path = pickFile(w, false);
|
||
if (!path.empty())
|
||
{
|
||
v->draft = loadSettings(path, v->app->layouts);
|
||
fillSettings(w, v);
|
||
}
|
||
return 0;
|
||
}
|
||
if (id == 251)
|
||
{
|
||
auto s = collectSettings(w, v);
|
||
auto path = pickFile(w, true);
|
||
if (!path.empty())
|
||
saveSettings(path, s);
|
||
return 0;
|
||
}
|
||
if (id == 255)
|
||
{
|
||
showAutoOptions(w, v);
|
||
return 0;
|
||
}
|
||
if (id == 252)
|
||
{
|
||
showSnippets(*v->app);
|
||
return 0;
|
||
}
|
||
if (id == 253)
|
||
{
|
||
if (v->app->busy)
|
||
throw std::runtime_error("Wait for the current operation");
|
||
v->draft = collectSettings(w, v);
|
||
v->app->reloadLayouts();
|
||
v->draft.direct = v->app->settings.direct;
|
||
fillSettings(w, v);
|
||
return 0;
|
||
}
|
||
if (id == 254 && v->app->history)
|
||
{
|
||
if (MessageBoxW(w, L"Delete all clipboard history, including pinned items?", L"Clear history",
|
||
MB_YESNO | MB_ICONWARNING) == IDYES)
|
||
v->app->history->clear();
|
||
return 0;
|
||
}
|
||
break;
|
||
}
|
||
case WM_CLOSE:
|
||
DestroyWindow(w);
|
||
return 0;
|
||
case WM_NCDESTROY:
|
||
disposeFont(w);
|
||
v->app->settingsWindow = nullptr;
|
||
delete v;
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
}
|
||
catch (const std::exception &e)
|
||
{
|
||
error(w, e);
|
||
}
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
struct SnippetView
|
||
{
|
||
App *app;
|
||
std::vector<Snippet> snippets;
|
||
int selection = -1;
|
||
};
|
||
void fillSnippets(HWND w, SnippetView *v)
|
||
{
|
||
SendDlgItemMessageW(w, 301, LB_RESETCONTENT, 0, 0);
|
||
for (auto &sn : v->snippets)
|
||
SendDlgItemMessageW(w, 301, LB_ADDSTRING, 0, (LPARAM)sn.trigger.c_str());
|
||
if (v->selection >= 0)
|
||
SendDlgItemMessageW(w, 301, LB_SETCURSEL, v->selection, 0);
|
||
}
|
||
void collectSnippet(HWND w, SnippetView *v)
|
||
{
|
||
if (v->selection >= 0 && v->selection < (int)v->snippets.size())
|
||
{
|
||
v->snippets[v->selection] = {value(w, 302), value(w, 303)};
|
||
}
|
||
}
|
||
LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||
{
|
||
if (msg == WM_DPICHANGED)
|
||
{
|
||
resizeForDpi(w, wp, lp);
|
||
return 0;
|
||
}
|
||
auto *v = (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
||
if (msg == WM_NCCREATE)
|
||
{
|
||
auto *app = (App *)((CREATESTRUCTW *)lp)->lpCreateParams;
|
||
v = new SnippetView{app, app->settings.snippets};
|
||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v);
|
||
return TRUE;
|
||
}
|
||
if (!v)
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
try
|
||
{
|
||
switch (msg)
|
||
{
|
||
case WM_COMMAND:
|
||
{
|
||
int id = LOWORD(wp);
|
||
if (id == 301 && HIWORD(wp) == LBN_SELCHANGE)
|
||
{
|
||
collectSnippet(w, v);
|
||
v->selection = (int)SendDlgItemMessageW(w, 301, LB_GETCURSEL, 0, 0);
|
||
if (v->selection >= 0)
|
||
{
|
||
value(w, 302, v->snippets[v->selection].trigger);
|
||
value(w, 303, v->snippets[v->selection].text);
|
||
}
|
||
return 0;
|
||
}
|
||
if (id == 310)
|
||
{
|
||
collectSnippet(w, v);
|
||
v->snippets.push_back({L";new", L""});
|
||
v->selection = (int)v->snippets.size() - 1;
|
||
fillSnippets(w, v);
|
||
value(w, 302, L";new");
|
||
value(w, 303, L"");
|
||
SetFocus(GetDlgItem(w, 302));
|
||
return 0;
|
||
}
|
||
if (id == 311)
|
||
{
|
||
if (v->selection >= 0)
|
||
{
|
||
v->snippets.erase(v->snippets.begin() + v->selection);
|
||
v->selection = -1;
|
||
fillSnippets(w, v);
|
||
value(w, 302, L"");
|
||
value(w, 303, L"");
|
||
}
|
||
return 0;
|
||
}
|
||
if (id == 312)
|
||
{
|
||
SendDlgItemMessageW(w, 303, EM_REPLACESEL, TRUE, (LPARAM)L"{cursor}");
|
||
return 0;
|
||
}
|
||
if (id == IDOK)
|
||
{
|
||
collectSnippet(w, v);
|
||
auto s = v->app->settings;
|
||
s.snippets = v->snippets;
|
||
if (v->app->busy)
|
||
throw std::runtime_error("Wait for the current operation");
|
||
v->app->applySettings(s);
|
||
if (v->app->settingsWindow)
|
||
{
|
||
auto *parent = (SettingsView *)GetWindowLongPtrW(v->app->settingsWindow, GWLP_USERDATA);
|
||
parent->draft.snippets = s.snippets;
|
||
}
|
||
DestroyWindow(w);
|
||
return 0;
|
||
}
|
||
if (id == IDCANCEL)
|
||
{
|
||
DestroyWindow(w);
|
||
return 0;
|
||
}
|
||
break;
|
||
}
|
||
case WM_CLOSE:
|
||
DestroyWindow(w);
|
||
return 0;
|
||
case WM_NCDESTROY:
|
||
disposeFont(w);
|
||
v->app->snippetsWindow = nullptr;
|
||
delete v;
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
}
|
||
catch (const std::exception &e)
|
||
{
|
||
error(w, e);
|
||
}
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||
{
|
||
if (msg == WM_SIZE)
|
||
{
|
||
MoveWindow(GetDlgItem(w, 1), 10, 10, LOWORD(lp) - 20, HIWORD(lp) - 20, TRUE);
|
||
return 0;
|
||
}
|
||
if (msg == WM_NCDESTROY)
|
||
disposeFont(w);
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
} // namespace
|
||
void refreshHistory(App &app)
|
||
{
|
||
if (app.historyWindow)
|
||
{
|
||
auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA);
|
||
search(app.historyWindow, v);
|
||
}
|
||
}
|
||
void showHistory(App &app)
|
||
{
|
||
if (!app.history)
|
||
{
|
||
app.report(L"Clipboard history is unavailable. See the log in the data folder.");
|
||
return;
|
||
}
|
||
HWND foreground = GetForegroundWindow();
|
||
if (app.historyWindow && IsWindowVisible(app.historyWindow) && foreground == app.historyWindow)
|
||
{
|
||
auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA);
|
||
if (v->items.size() > 1)
|
||
app.pasteEntry(v->items[1].id, false);
|
||
return;
|
||
}
|
||
if (foreground != app.window && foreground != app.historyWindow)
|
||
app.pasteTarget = foreground;
|
||
if (!app.historyWindow)
|
||
{
|
||
HWND w = makeWindow(app, L"KSwitcher.History", L"Clipboard history", historyProc, 760, 500);
|
||
app.historyWindow = w;
|
||
((HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA))->rich = createRichText(w, 105, false);
|
||
label(w, L"Search clipboard history", 16, 12, 500);
|
||
edit(w, 101, 16, 38, 728);
|
||
auto list = control(w, L"LISTBOX", L"",
|
||
WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER |
|
||
LBS_OWNERDRAWFIXED | LBS_HASSTRINGS,
|
||
102, 16, 76, 728, 360);
|
||
SendMessageW(list, LB_SETITEMHEIGHT, 0, scale(w, 88));
|
||
button(w, L"Paste", 110, 16, 452, 90);
|
||
button(w, L"Plain text", 111, 114, 452, 100);
|
||
button(w, L"Pin / unpin", 112, 222, 452, 110);
|
||
button(w, L"Delete", 113, 340, 452, 90);
|
||
button(w, L"Open item", 114, 438, 452, 100);
|
||
SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w);
|
||
SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w);
|
||
}
|
||
POINT cursor;
|
||
GetCursorPos(&cursor);
|
||
MONITORINFO monitor{sizeof(monitor)};
|
||
GetMonitorInfoW(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &monitor);
|
||
RECT r;
|
||
GetWindowRect(app.historyWindow, &r);
|
||
int x = std::max(monitor.rcWork.left, std::min(cursor.x, monitor.rcWork.right - (r.right - r.left)));
|
||
int y = std::max(monitor.rcWork.top, std::min(cursor.y, monitor.rcWork.bottom - (r.bottom - r.top)));
|
||
SetWindowPos(app.historyWindow, HWND_TOP, x, y, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW);
|
||
SetForegroundWindow(app.historyWindow);
|
||
SetFocus(GetDlgItem(app.historyWindow, 101));
|
||
refreshHistory(app);
|
||
}
|
||
namespace
|
||
{
|
||
constexpr int LogChoice = 601, LogRows = 602, LogRefresh = 603, LogStatus = 604, LogCopy = 605,
|
||
LogClear = 606;
|
||
LRESULT CALLBACK logListProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||
{
|
||
// Header notifications go to the ListView, not its top-level parent.
|
||
if (msg == WM_NOTIFY && ((NMHDR *)lp)->hwndFrom == ListView_GetHeader(w) &&
|
||
((NMHDR *)lp)->code == NM_CUSTOMDRAW)
|
||
{
|
||
auto *draw = (NMCUSTOMDRAW *)lp;
|
||
if (draw->dwDrawStage == CDDS_PREPAINT)
|
||
{
|
||
FillRect(draw->hdc, &draw->rc, GetSysColorBrush(COLOR_BTNFACE));
|
||
return CDRF_NOTIFYITEMDRAW;
|
||
}
|
||
if (draw->dwDrawStage == CDDS_ITEMPREPAINT)
|
||
{
|
||
int saved = SaveDC(draw->hdc);
|
||
FillRect(draw->hdc, &draw->rc, GetSysColorBrush(COLOR_BTNFACE));
|
||
RECT edge = draw->rc;
|
||
edge.top = edge.bottom - 1;
|
||
FillRect(draw->hdc, &edge, GetSysColorBrush(COLOR_3DSHADOW));
|
||
edge = draw->rc;
|
||
edge.left = edge.right - 1;
|
||
FillRect(draw->hdc, &edge, GetSysColorBrush(COLOR_3DSHADOW));
|
||
wchar_t title[128]{};
|
||
HDITEMW item{};
|
||
item.mask = HDI_TEXT;
|
||
item.pszText = title;
|
||
item.cchTextMax = 128;
|
||
Header_GetItem(draw->hdr.hwndFrom, (int)draw->dwItemSpec, &item);
|
||
auto headerFont = (HFONT)SendMessageW(draw->hdr.hwndFrom, WM_GETFONT, 0, 0);
|
||
if (headerFont)
|
||
SelectObject(draw->hdc, headerFont);
|
||
SetBkMode(draw->hdc, TRANSPARENT);
|
||
SetTextColor(draw->hdc, GetSysColor(COLOR_BTNTEXT));
|
||
RECT text = draw->rc;
|
||
InflateRect(&text, -scale(w, 6), 0);
|
||
DrawTextW(draw->hdc, title, -1, &text, DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS | DT_NOPREFIX);
|
||
RestoreDC(draw->hdc, saved);
|
||
return CDRF_SKIPDEFAULT;
|
||
}
|
||
}
|
||
if (msg == WM_NCDESTROY)
|
||
RemoveWindowSubclass(w, logListProc, 1);
|
||
return DefSubclassProc(w, msg, wp, lp);
|
||
}
|
||
void copyLogRows(HWND w, App &app)
|
||
{
|
||
HWND list = GetDlgItem(w, LogRows);
|
||
std::wstring copied;
|
||
for (int row = ListView_GetNextItem(list, -1, LVNI_SELECTED); row >= 0;
|
||
row = ListView_GetNextItem(list, row, LVNI_SELECTED))
|
||
{
|
||
for (int column = 0; column < 4; ++column)
|
||
{
|
||
std::vector<wchar_t> text(32768);
|
||
ListView_GetItemText(list, row, column, text.data(), (int)text.size());
|
||
if (column)
|
||
copied += L'\t';
|
||
copied += text.data();
|
||
}
|
||
copied += L"\r\n";
|
||
}
|
||
if (!copied.empty())
|
||
app.ownSequence = writeClipboard(app.window, textClip(copied));
|
||
}
|
||
void refreshLogView(HWND w, App &app)
|
||
{
|
||
const bool previous = SendDlgItemMessageW(w, LogChoice, CB_GETCURSEL, 0, 0) == 1;
|
||
std::wstring contents;
|
||
try
|
||
{
|
||
contents = readDiagnosticLog(previous);
|
||
}
|
||
catch (const std::exception &e)
|
||
{
|
||
contents = L"Could not read the log: " + wide(e.what());
|
||
}
|
||
HWND list = GetDlgItem(w, LogRows);
|
||
SendMessageW(list, WM_SETREDRAW, FALSE, 0);
|
||
ListView_DeleteAllItems(list);
|
||
std::wistringstream lines(contents);
|
||
std::wstring line, timestamp;
|
||
int row = 0;
|
||
while (std::getline(lines, line))
|
||
{
|
||
if (!line.empty() && line.back() == L'\r')
|
||
line.pop_back();
|
||
if (line.empty())
|
||
continue;
|
||
auto firstSpace = line.find(L' ');
|
||
auto secondSpace = firstSpace == line.npos ? line.npos : line.find(L' ', firstSpace + 1);
|
||
if (iswdigit(line.front()) && line.find(L'-') < firstSpace && secondSpace != line.npos)
|
||
{
|
||
timestamp = line.substr(0, secondSpace);
|
||
line.erase(0, secondSpace + 1);
|
||
}
|
||
auto id = line.find(L" id=");
|
||
std::wstring attempt;
|
||
if (id != line.npos)
|
||
{
|
||
auto end = line.find(L' ', id + 4);
|
||
attempt = line.substr(id + 4, end == line.npos ? end : end - id - 4);
|
||
}
|
||
std::wstring event = line;
|
||
if (event.starts_with(L" ageMs="))
|
||
{
|
||
auto end = event.find(L' ', 8);
|
||
if (end != event.npos)
|
||
event.erase(0, end + 1);
|
||
}
|
||
auto equals = event.find(L'=');
|
||
if (equals != event.npos)
|
||
{
|
||
auto key = event.rfind(L' ', equals);
|
||
if (key != event.npos)
|
||
event.resize(key);
|
||
}
|
||
LVITEMW item{};
|
||
item.mask = LVIF_TEXT;
|
||
item.iItem = row;
|
||
item.pszText = timestamp.data();
|
||
ListView_InsertItem(list, &item);
|
||
ListView_SetItemText(list, row, 1, attempt.data());
|
||
ListView_SetItemText(list, row, 2, event.data());
|
||
ListView_SetItemText(list, row, 3, line.data());
|
||
++row;
|
||
}
|
||
SendMessageW(list, WM_SETREDRAW, TRUE, 0);
|
||
InvalidateRect(list, nullptr, TRUE);
|
||
if (row)
|
||
ListView_EnsureVisible(list, row - 1, FALSE);
|
||
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: problems and suspicious repeats only."
|
||
: L"Logging disabled. Showing previously saved entries.");
|
||
}
|
||
LRESULT CALLBACK logProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||
{
|
||
auto *app = reinterpret_cast<App *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||
if (msg == WM_NCCREATE)
|
||
{
|
||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)((CREATESTRUCTW *)lp)->lpCreateParams);
|
||
return TRUE;
|
||
}
|
||
if (!app)
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
switch (msg)
|
||
{
|
||
case WM_COMMAND:
|
||
if ((LOWORD(wp) == LogRefresh && HIWORD(wp) == BN_CLICKED) ||
|
||
(LOWORD(wp) == LogChoice && HIWORD(wp) == CBN_SELCHANGE))
|
||
{
|
||
refreshLogView(w, *app);
|
||
return 0;
|
||
}
|
||
if (LOWORD(wp) == LogCopy && HIWORD(wp) == BN_CLICKED)
|
||
{
|
||
try { copyLogRows(w, *app); }
|
||
catch (const std::exception &e) { error(w, e); }
|
||
return 0;
|
||
}
|
||
if (LOWORD(wp) == LogClear && HIWORD(wp) == BN_CLICKED)
|
||
{
|
||
try
|
||
{
|
||
clearDiagnosticLog(SendDlgItemMessageW(w, LogChoice, CB_GETCURSEL, 0, 0) == 1);
|
||
refreshLogView(w, *app);
|
||
}
|
||
catch (const std::exception &e) { error(w, e); }
|
||
return 0;
|
||
}
|
||
if (LOWORD(wp) != IDCANCEL || HIWORD(wp) != BN_CLICKED)
|
||
break;
|
||
[[fallthrough]];
|
||
case WM_CLOSE:
|
||
DestroyWindow(w);
|
||
return 0;
|
||
case WM_NOTIFY:
|
||
if (((NMHDR *)lp)->idFrom == LogRows && ((NMHDR *)lp)->code == LVN_KEYDOWN)
|
||
{
|
||
auto key = ((NMLVKEYDOWN *)lp)->wVKey;
|
||
if (GetKeyState(VK_CONTROL) & 0x8000)
|
||
{
|
||
if (key == 'A')
|
||
ListView_SetItemState(GetDlgItem(w, LogRows), -1, LVIS_SELECTED, LVIS_SELECTED);
|
||
if (key == 'C')
|
||
{
|
||
try { copyLogRows(w, *app); }
|
||
catch (const std::exception &e) { error(w, e); }
|
||
}
|
||
}
|
||
}
|
||
return 0;
|
||
case WM_DPICHANGED:
|
||
resizeForDpi(w, wp, lp);
|
||
return 0;
|
||
case WM_NCDESTROY:
|
||
app->logWindow = nullptr;
|
||
disposeFont(w);
|
||
break;
|
||
}
|
||
return DefWindowProcW(w, msg, wp, lp);
|
||
}
|
||
}
|
||
void showLogs(App &app)
|
||
{
|
||
if (app.logWindow)
|
||
{
|
||
showRequestedWindow(app.logWindow);
|
||
activateWindow(app.logWindow);
|
||
return;
|
||
}
|
||
auto w = makeWindow(app, L"KSwitcher.Logs", L"KSwitcher log", logProc, 900, 590);
|
||
app.logWindow = w;
|
||
auto choice = control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST, LogChoice, 16, 16, 300, 140);
|
||
SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Current log — kswitcher.log");
|
||
SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Previous log — kswitcher.log.1");
|
||
SendMessageW(choice, CB_SETCURSEL, 0, 0);
|
||
button(w, L"Refresh", LogRefresh, 330, 14, 110);
|
||
button(w, L"Copy selected", LogCopy, 454, 14, 140);
|
||
button(w, L"Clear log", LogClear, 608, 14, 110);
|
||
control(w, L"STATIC", L"", 0, LogStatus, 16, 52, 860, 22);
|
||
INITCOMMONCONTROLSEX common{sizeof(common), ICC_LISTVIEW_CLASSES};
|
||
InitCommonControlsEx(&common);
|
||
auto list = control(w, WC_LISTVIEWW, L"", WS_TABSTOP | WS_BORDER | LVS_REPORT | LVS_SHOWSELALWAYS,
|
||
LogRows, 16, 82, 868, 454);
|
||
ListView_SetExtendedListViewStyle(list, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
|
||
SetWindowSubclass(list, logListProc, 1, 0);
|
||
const wchar_t *titles[] = {L"Time", L"Attempt", L"Event", L"Details"};
|
||
const int widths[] = {155, 65, 195, 900};
|
||
for (int i = 0; i < 4; ++i)
|
||
{
|
||
LVCOLUMNW column{};
|
||
column.mask = LVCF_TEXT | LVCF_WIDTH;
|
||
column.pszText = const_cast<wchar_t *>(titles[i]);
|
||
column.cx = scale(w, widths[i]);
|
||
ListView_InsertColumn(list, i, &column);
|
||
}
|
||
label(w, L"Select rows to copy. Ctrl+A selects all; Ctrl+C copies selected rows.", 16, 551, 720);
|
||
button(w, L"Close", IDCANCEL, 774, 548, 110);
|
||
refreshLogView(w, app);
|
||
showRequestedWindow(w);
|
||
activateWindow(w);
|
||
}
|
||
void showSettings(App &app, bool visible)
|
||
{
|
||
if (app.settingsWindow)
|
||
{
|
||
ShowWindow(app.settingsWindow, SW_RESTORE);
|
||
HWND target = app.settingsWindow;
|
||
if (!IsWindowEnabled(target))
|
||
target = GetLastActivePopup(target);
|
||
activateWindow(target);
|
||
return;
|
||
}
|
||
HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 780, 660);
|
||
app.settingsWindow = w;
|
||
label(w, L"Text conversion", 18, 16);
|
||
checkbox(w, L"Convert recently typed text when nothing is selected", 201, 18, 43, 390);
|
||
checkbox(w, L"Switch Windows input layout after conversion", 202, 18, 70, 390);
|
||
checkbox(w, L"Record clipboard history", 203, 18, 106);
|
||
checkbox(w, L"Start with Windows", 204, 18, 133);
|
||
checkbox(w, L"Start with Windows as administrator", 206, 18, 160, 395);
|
||
checkbox(w, L"Keep screen awake", 205, 18, 184, 210);
|
||
checkbox(w, L"Write log", 208, 240, 184, 160);
|
||
label(w, L"Click a shortcut and press the desired keys", 420, 16, 350);
|
||
const wchar_t *names[] = {L"Next layout", L"Change case", L"Transliterate", L"Undo",
|
||
L"Clipboard history"};
|
||
for (int i = 0; i < 5; ++i)
|
||
{
|
||
label(w, names[i], 420, 48 + i * 30, 115);
|
||
hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220);
|
||
}
|
||
checkbox(w, L"Automatically correct layout (experimental)", 207, 420, 195, 350);
|
||
label(w, L"Layout cycle — top to bottom", 18, 210, 400);
|
||
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220,
|
||
18, 232, 470, 190);
|
||
button(w, L"Move up", 232, 504, 232, 120);
|
||
button(w, L"Move down", 233, 632, 232, 128);
|
||
checkbox(w, L"Include in cycle", 230, 504, 275, 240);
|
||
label(w, L"Direct layout shortcut", 504, 313, 240);
|
||
hotkeyEdit(app, w, 231, 504, 340, 256);
|
||
label(w, L"Backspace / Delete clears a shortcut", 504, 365, 256);
|
||
button(w, L"Refresh layouts", 253, 504, 384, 150);
|
||
label(w, L"History limits", 18, 444);
|
||
label(w, L"Items", 18, 478, 50);
|
||
edit(w, 240, 70, 474, 90);
|
||
label(w, L"Total MiB", 180, 478, 75);
|
||
edit(w, 241, 260, 474, 90);
|
||
label(w, L"Per item MiB", 370, 478, 100);
|
||
edit(w, 242, 475, 474, 80);
|
||
button(w, L"Clear history…", 254, 580, 474, 180);
|
||
label(w, L"Exclude applications (process names, comma-separated)", 18, 516, 700);
|
||
edit(w, 243, 18, 544, 742);
|
||
button(w, L"Auto-correction options…", 255, 18, 575, 240);
|
||
button(w, L"View log…", 256, 278, 575, 220);
|
||
button(w, L"Import settings…", 250, 18, 606, 145);
|
||
button(w, L"Export settings…", 251, 173, 606, 145);
|
||
button(w, L"Snippets…", 252, 328, 606, 110);
|
||
button(w, L"Save", IDOK, 548, 606, 100);
|
||
button(w, L"Cancel", IDCANCEL, 660, 606, 100);
|
||
fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
||
if (visible)
|
||
{
|
||
showRequestedWindow(w);
|
||
activateWindow(w);
|
||
}
|
||
}
|
||
void showSnippets(App &app)
|
||
{
|
||
if (app.snippetsWindow)
|
||
{
|
||
SetForegroundWindow(app.snippetsWindow);
|
||
return;
|
||
}
|
||
HWND w = makeWindow(app, L"KSwitcher.Snippets", L"Text snippets", snippetProc, 740, 520);
|
||
app.snippetsWindow = w;
|
||
label(w, L"Triggers", 16, 16);
|
||
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 301,
|
||
16, 44, 200, 380);
|
||
button(w, L"Add", 310, 16, 438, 90);
|
||
button(w, L"Delete", 311, 116, 438, 100);
|
||
label(w, L"Trigger (for example ;addr)", 236, 16, 470);
|
||
edit(w, 302, 236, 44, 488);
|
||
label(w, L"Replacement text", 236, 82, 480);
|
||
edit(w, 303, 236, 110, 488, 270, ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL | WS_VSCROLL);
|
||
label(w, L"Use {cursor} once; {{cursor}} inserts the literal marker.", 236, 392, 488);
|
||
button(w, L"Insert cursor marker", 312, 236, 430, 190);
|
||
button(w, L"Save", IDOK, 510, 478, 100);
|
||
button(w, L"Cancel", IDCANCEL, 624, 478, 100);
|
||
fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
||
showRequestedWindow(w);
|
||
SetForegroundWindow(w);
|
||
}
|
||
void showShortcuts(App &app)
|
||
{
|
||
auto &s = app.settings;
|
||
std::wstring text = L"Next layout: " + s.toggle + L"\nChange case: " + s.casing + L"\nTransliterate: " +
|
||
s.translit + L"\nUndo: " + s.undo + L"\nClipboard history: " + s.clipboard + L"\n";
|
||
for (size_t i = 0; i < std::min(app.layouts.size(), s.direct.size()); ++i)
|
||
if (!s.direct[i].empty())
|
||
text += L"\n" + app.layouts[i].name + L": " + s.direct[i];
|
||
text += L"\n\nHistory: Enter to paste, Shift+Enter for plain text,\nCtrl+1…9 to paste, Ctrl+P to pin, "
|
||
L"Delete to remove,\nCtrl+O to open the full item, Escape to close.";
|
||
MessageBoxW(app.window, text.c_str(), L"Keyboard shortcuts", MB_OK);
|
||
}
|
||
void showEntry(App &app, const std::wstring &id)
|
||
{
|
||
try
|
||
{
|
||
auto clip = app.history->load(id);
|
||
auto text = clip.text();
|
||
if (text.empty())
|
||
{
|
||
MessageBoxW(app.historyWindow,
|
||
L"This item contains an image or files. Use the preview or paste it into the "
|
||
L"destination application.",
|
||
L"Clipboard item", MB_OK);
|
||
return;
|
||
}
|
||
HWND w = makeWindow(app, L"KSwitcher.Entry", L"Clipboard item", entryProc, 720, 500,
|
||
WS_THICKFRAME | WS_MAXIMIZEBOX);
|
||
auto field = edit(w, 1, 10, 10, 700, 480, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL);
|
||
SetWindowTextW(field, text.c_str());
|
||
showRequestedWindow(w);
|
||
}
|
||
catch (const std::exception &e)
|
||
{
|
||
error(app.historyWindow, e);
|
||
}
|
||
}
|
||
} // namespace ks
|