Add configurable offline layout correction and latest Windows release

This commit is contained in:
2026-10-27 23:50:04 +02:00
parent f683e4178c
commit f1af998029
25 changed files with 142458 additions and 27 deletions
+170 -5
View File
@@ -8,6 +8,7 @@
#include <map>
#include <richedit.h>
#include <set>
#include <shellapi.h>
#include <sstream>
namespace ks
@@ -648,6 +649,7 @@ void fillSettings(HWND w, SettingsView *v)
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);
@@ -695,6 +697,7 @@ Settings collectSettings(HWND w, SettingsView *v)
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);
@@ -723,6 +726,158 @@ Settings collectSettings(HWND w, SettingsView *v)
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)
@@ -802,6 +957,11 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
saveSettings(path, s);
return 0;
}
if (id == 255)
{
showAutoOptions(w, v);
return 0;
}
if (id == 252)
{
showSnippets(*v->app);
@@ -1040,7 +1200,10 @@ void showSettings(App &app)
if (app.settingsWindow)
{
ShowWindow(app.settingsWindow, SW_RESTORE);
SetForegroundWindow(app.settingsWindow);
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);
@@ -1060,6 +1223,7 @@ void showSettings(App &app)
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);
@@ -1080,14 +1244,15 @@ void showSettings(App &app)
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"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);
showRequestedWindow(w);
activateWindow(w);
}
void showSnippets(App &app)
{
@@ -1112,7 +1277,7 @@ void showSnippets(App &app)
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);
showRequestedWindow(w);
SetForegroundWindow(w);
}
void showShortcuts(App &app)
@@ -1145,7 +1310,7 @@ void showEntry(App &app, const std::wstring &id)
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);
showRequestedWindow(w);
}
catch (const std::exception &e)
{