Compare commits

...

2 Commits

12 changed files with 112 additions and 18 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.12 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.14 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
Binary file not shown.
+8 -7
View File
@@ -1,16 +1,17 @@
# Latest Windows release
## v0.6.12 — 2026-10-30
## v0.6.14 — 2026-10-30
## Changes
- Start automatic layout correction immediately after Space and preserve subsequent typing while the correction runs.
- Accept identical corrected words shared by multiple selected languages; use the selected language order to choose the target layout. Different valid spellings remain ambiguous.
- Run the log viewer in the configurator process, separate from the resident input hooks. Clearing logs remains coordinated with the resident process.
- Keep the native four-column ListView. Resize the table with its window, anchor the footer and Close button, and make Details fill the remaining width.
- Exclude copied diagnostic rows from clipboard history.
- Restore resizing for the automatic-correction settings dialog with an adaptive layout.
- Open the dialog with larger personal-dictionary fields. The three dictionaries share additional width evenly and receive additional height as the window grows.
- Expand exclusion lists and punctuation fields horizontally, and move lower settings and action buttons with the window while preserving spacing.
- Retain scrolling when the dialog must fit a small screen.
Includes the earlier automatic-correction and log-viewer improvements. Existing modern releases update directly to this version without installing intermediate releases.
## Validation
Native and input regression suites, screenshot UI and updater checks, log-viewer integration, continuous-typing integration, and isolated configurator/log process checks. Manual development-build checks covered window dragging and automatic Details column width.
220 native regression checks, 472 input/UI checks including dictionary sizing and footer placement, screenshot editor and notification registration checks, 45 updater checks and updater progress checks. The resized dialog was also confirmed in the development build by the user.
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
+1 -1
View File
@@ -1 +1 @@
866de2d1d6fe121a7845760519f934bf0bd7820bbd11734a14c454b69a6bb8ad KSwitcher-win-x64.zip
4939b83b2cd641e969107091012fe12340b09d73ec1b7f7292c6809d226e05f5 KSwitcher-win-x64.zip
+3 -3
View File
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1"
1 VERSIONINFO
FILEVERSION 0,6,12,0
PRODUCTVERSION 0,6,12,0
FILEVERSION 0,6,14,0
PRODUCTVERSION 0,6,14,0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.12"
VALUE "FileVersion", "0.6.14"
END
END
BLOCK "VarFileInfo"
+7 -3
View File
@@ -940,6 +940,7 @@ void App::transform(int command, int snippet)
std::wstring predicted;
for (wchar_t key : recent.keys)
predicted += key == L' ' ? L' ' : available[hint].at(physicalLayout().code(key));
expected = predicted;
if (!autoCorrector.suggest(predicted, hint, available, config) ||
!autoCorrectionAllowed(target, config) || !valid())
return;
@@ -979,9 +980,12 @@ void App::transform(int command, int snippet)
autoSelected = true;
Sleep(25);
text = tx.copy();
// Some editors first select only ") ". Extend by whole-word
// boundaries, then still verify against the complete tracked run.
for (int i = 0; i < 3 && autoClosingSelection(text, config) && tx.valid(); ++i)
// Editors may split hj,bnm at the comma. Extend only a verified
// suffix of the tracked input, and still require a complete match.
for (int i = 0; i < 3 && tx.valid() &&
matchAutoInput(recent, text, available, physicalLayout(), config) < 0 &&
(autoClosingSelection(text, config) ||
(!text.empty() && text.size() < expected.size() && expected.ends_with(text))); ++i)
{
if (!chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT))
break;
+5 -1
View File
@@ -152,7 +152,11 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
suffix.insert(suffix.begin(), word.back());
word.pop_back();
}
if (!safeAutoWord(word, settings.autoMinLength) || suffix.size() > 6 || rejected.contains(lower(word)))
// Punctuation keys can be letters in the target layout (for example ',' -> 'б').
// Validate letters and capitalization after conversion, not before it.
if (settings.autoMinLength < 1 || settings.autoMinLength > 32 ||
word.size() < (size_t)settings.autoMinLength || word.size() > 32 ||
suffix.size() > 6 || rejected.contains(lower(word)))
return {};
for (auto &never : settings.autoNever)
if (lower(never) == lower(word))
+30 -1
View File
@@ -35,6 +35,7 @@ struct WindowLayout
int wheelRemainder{}, horizontalWheelRemainder{};
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
HWND stretch{}, bottomLeft{}, bottomRight{};
bool autoOptions{};
};
WindowLayout *windowLayoutState(HWND w)
{
@@ -70,7 +71,7 @@ void arrangeWindow(HWND w)
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
}
RECT client{}; GetClientRect(w, &client);
bool expandable = layout->history || layout->stretch;
bool expandable = layout->history || layout->stretch || layout->autoOptions;
int extraWidth = expandable ? std::max(0, int(client.right) - scale(w, layout->width)) : 0;
int extraHeight = expandable ? std::max(0, int(client.bottom) - scale(w, layout->height)) : 0;
struct Move { HWND child; RECT rect; };
@@ -89,6 +90,28 @@ void arrangeWindow(HWND w)
}
if (child == layout->bottomLeft || child == layout->bottomRight)
OffsetRect(&next, child == layout->bottomRight ? extraWidth : 0, extraHeight);
if (layout->autoOptions)
{
if (r.top >= 396) OffsetRect(&next, 0, extraHeight);
if (r.top == 302 || r.top == 328 || (id >= 401 && id <= 403))
{
int column = id >= 401 && id <= 403 ? id - 401 : (r.left - 18) / 178;
int leftExtra = extraWidth * column / 3;
OffsetRect(&next, leftExtra, 0);
next.right += extraWidth * (column + 1) / 3 - leftExtra;
if (r.top == 328) next.bottom += extraHeight;
}
else if (r.top == 474 || r.top == 500)
{
bool rightColumn = r.left >= 290;
OffsetRect(&next, rightColumn ? extraWidth / 2 : 0, 0);
next.right += rightColumn ? extraWidth - extraWidth / 2 : extraWidth / 2;
}
else if (id == 406 || id == 410 || id == IDOK || id == IDCANCEL)
OffsetRect(&next, extraWidth, 0);
else if (r.right - r.left >= 500)
next.right += extraWidth;
}
RECT current{}; GetWindowRect(child, &current);
MapWindowPoints(nullptr, w, reinterpret_cast<POINT *>(&current), 2);
if (!EqualRect(&current, &next)) moves.push_back({child, next});
@@ -1073,6 +1096,7 @@ void showAutoOptions(HWND parent, SettingsView *v)
{
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc,
560, 680);
windowLayoutState(w)->autoOptions = true;
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v});
label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525);
@@ -1113,6 +1137,11 @@ void showAutoOptions(HWND parent, SettingsView *v)
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);
RECT initial{};
GetWindowRect(w, &initial);
initial.right += scale(w, 200);
initial.bottom += scale(w, 140);
fitToMonitor(w, initial);
EnableWindow(parent, FALSE);
showRequestedWindow(w);
SetForegroundWindow(w);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.12";
inline constexpr wchar_t AppVersion[] = L"0.6.14";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+29
View File
@@ -598,6 +598,35 @@ int main(int argc, char **argv) {
auto autoOptions=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
require(autoOptions!=nullptr,"auto-correction options open");
ShowWindow(autoOptions,SW_HIDE);
{
require((GetWindowLongPtrW(autoOptions,GWL_STYLE)&WS_THICKFRAME)!=0,
"auto-correction dialog supports resizing");
RECT original{}, before[3]{}, after[3]{}, buttonBefore{}, buttonAfter{};
GetWindowRect(autoOptions,&original);
// Use a client size above the scrolling baseline at every DPI.
UINT dpi=GetDpiForWindow(autoOptions);
SetWindowPos(autoOptions,nullptr,0,0,MulDiv(800,dpi,96),MulDiv(900,dpi,96),
SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
RECT baseline{}; GetWindowRect(autoOptions,&baseline);
for(int i=0;i<3;++i) GetWindowRect(GetDlgItem(autoOptions,411+i),&before[i]);
GetWindowRect(GetDlgItem(autoOptions,IDOK),&buttonBefore);
SetWindowPos(autoOptions,nullptr,0,0,baseline.right-baseline.left+240,
baseline.bottom-baseline.top+120,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
for(int i=0;i<3;++i) {
GetWindowRect(GetDlgItem(autoOptions,411+i),&after[i]);
require(after[i].right-after[i].left==before[i].right-before[i].left+80 &&
after[i].bottom-after[i].top==before[i].bottom-before[i].top+120,
"dictionary columns share extra width and receive extra height");
}
GetWindowRect(GetDlgItem(autoOptions,IDOK),&buttonAfter);
require(buttonAfter.left-buttonBefore.left==240 && buttonAfter.top-buttonBefore.top==120,
"auto-correction buttons follow bottom-right corner");
require(after[0].right<after[1].left && after[1].right<after[2].left &&
after[2].bottom<buttonAfter.top,
"resized dictionary fields and footer do not overlap");
SetWindowPos(autoOptions,nullptr,0,0,original.right-original.left,original.bottom-original.top,
SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
}
for(int id:{411,412,413}) {
RECT control{},client{};
GetWindowRect(GetDlgItem(autoOptions,id),&control);
+18
View File
@@ -334,7 +334,25 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
stage = 21;
break;
case 21:
{
require(text() == L"привет а", "automatic correction preserves immediate next-word typing without a pause");
auto uk = std::find_if(app->layouts.begin(), app->layouts.end(), [](const auto &layout) {
return ks::layoutLanguage(layout) == L"uk";
});
require(uk != app->layouts.end(), "Ukrainian layout available for punctuation-key regression");
app->settings.autoLanguages = {L"en", L"uk"};
app->settings.cycle = {app->layouts[en].id, uk->id};
app->settings.autoWords[L"uk"] = {L"робить"};
app->autoCorrector = ks::AutoCorrector{};
ActivateKeyboardLayout(app->layouts[en].handle, 0);
prepare(L"hj,bnm ", false);
record(L"hj,bnm ");
app->transform(ks::AutoCorrect);
stage = 22;
break;
}
case 22:
require(text() == L"робить ", "automatic correction selects and converts the whole word across an internal comma");
finish();
break;
}
+9
View File
@@ -276,6 +276,15 @@ int main(int argc, char **argv)
require(!supplemental.suggest(L"релызик ", 1, layouts, options),
"never-correct list takes precedence over personal word");
options.autoNever.clear();
options.autoWords[L"uk"].push_back(L"робить");
auto commaWord = supplemental.suggest(L"hj,bnm ", 0, layouts, options);
require(commaWord && commaWord->text == L"робить " && commaWord->target == 2,
"personal Ukrainian word accepts a punctuation key that maps to a letter");
require(!supplemental.suggest(L"hj,bnm123 ", 0, layouts, options),
"punctuation-key correction still rejects digits");
options.autoMinLength = 1;
require(!supplemental.suggest(L"ye ", 0, layouts, options),
"known English ye remains protected at minimum length one");
options.cycle = {L"en", L"ru"};
require(!supplemental.suggest(L"релызик ", 1, layouts, options),
"personal dictionary respects disabled target layouts");