Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39d3876fd2 | |||
| 4cdcc92eb7 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
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)
|
if(NOT WIN32)
|
||||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
Binary file not shown.
+8
-7
@@ -1,16 +1,17 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.12 — 2026-10-30
|
## v0.6.14 — 2026-10-30
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Start automatic layout correction immediately after Space and preserve subsequent typing while the correction runs.
|
- Restore resizing for the automatic-correction settings dialog with an adaptive layout.
|
||||||
- Accept identical corrected words shared by multiple selected languages; use the selected language order to choose the target layout. Different valid spellings remain ambiguous.
|
- Open the dialog with larger personal-dictionary fields. The three dictionaries share additional width evenly and receive additional height as the window grows.
|
||||||
- Run the log viewer in the configurator process, separate from the resident input hooks. Clearing logs remains coordinated with the resident process.
|
- Expand exclusion lists and punctuation fields horizontally, and move lower settings and action buttons with the window while preserving spacing.
|
||||||
- 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.
|
- Retain scrolling when the dialog must fit a small screen.
|
||||||
- Exclude copied diagnostic rows from clipboard history.
|
|
||||||
|
Includes the earlier automatic-correction and log-viewer improvements. Existing modern releases update directly to this version without installing intermediate releases.
|
||||||
|
|
||||||
## Validation
|
## 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.
|
**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.
|
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
866de2d1d6fe121a7845760519f934bf0bd7820bbd11734a14c454b69a6bb8ad KSwitcher-win-x64.zip
|
4939b83b2cd641e969107091012fe12340b09d73ec1b7f7292c6809d226e05f5 KSwitcher-win-x64.zip
|
||||||
|
|||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,6,12,0
|
FILEVERSION 0,6,14,0
|
||||||
PRODUCTVERSION 0,6,12,0
|
PRODUCTVERSION 0,6,14,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.6.12"
|
VALUE "FileVersion", "0.6.14"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+7
-3
@@ -940,6 +940,7 @@ void App::transform(int command, int snippet)
|
|||||||
std::wstring predicted;
|
std::wstring predicted;
|
||||||
for (wchar_t key : recent.keys)
|
for (wchar_t key : recent.keys)
|
||||||
predicted += key == L' ' ? L' ' : available[hint].at(physicalLayout().code(key));
|
predicted += key == L' ' ? L' ' : available[hint].at(physicalLayout().code(key));
|
||||||
|
expected = predicted;
|
||||||
if (!autoCorrector.suggest(predicted, hint, available, config) ||
|
if (!autoCorrector.suggest(predicted, hint, available, config) ||
|
||||||
!autoCorrectionAllowed(target, config) || !valid())
|
!autoCorrectionAllowed(target, config) || !valid())
|
||||||
return;
|
return;
|
||||||
@@ -979,9 +980,12 @@ void App::transform(int command, int snippet)
|
|||||||
autoSelected = true;
|
autoSelected = true;
|
||||||
Sleep(25);
|
Sleep(25);
|
||||||
text = tx.copy();
|
text = tx.copy();
|
||||||
// Some editors first select only ") ". Extend by whole-word
|
// Editors may split hj,bnm at the comma. Extend only a verified
|
||||||
// boundaries, then still verify against the complete tracked run.
|
// suffix of the tracked input, and still require a complete match.
|
||||||
for (int i = 0; i < 3 && autoClosingSelection(text, config) && tx.valid(); ++i)
|
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))
|
if (!chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT))
|
||||||
break;
|
break;
|
||||||
|
|||||||
+5
-1
@@ -152,7 +152,11 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
|
|||||||
suffix.insert(suffix.begin(), word.back());
|
suffix.insert(suffix.begin(), word.back());
|
||||||
word.pop_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 {};
|
return {};
|
||||||
for (auto &never : settings.autoNever)
|
for (auto &never : settings.autoNever)
|
||||||
if (lower(never) == lower(word))
|
if (lower(never) == lower(word))
|
||||||
|
|||||||
+30
-1
@@ -35,6 +35,7 @@ struct WindowLayout
|
|||||||
int wheelRemainder{}, horizontalWheelRemainder{};
|
int wheelRemainder{}, horizontalWheelRemainder{};
|
||||||
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
||||||
HWND stretch{}, bottomLeft{}, bottomRight{};
|
HWND stretch{}, bottomLeft{}, bottomRight{};
|
||||||
|
bool autoOptions{};
|
||||||
};
|
};
|
||||||
WindowLayout *windowLayoutState(HWND w)
|
WindowLayout *windowLayoutState(HWND w)
|
||||||
{
|
{
|
||||||
@@ -70,7 +71,7 @@ void arrangeWindow(HWND w)
|
|||||||
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
||||||
}
|
}
|
||||||
RECT client{}; GetClientRect(w, &client);
|
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 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;
|
int extraHeight = expandable ? std::max(0, int(client.bottom) - scale(w, layout->height)) : 0;
|
||||||
struct Move { HWND child; RECT rect; };
|
struct Move { HWND child; RECT rect; };
|
||||||
@@ -89,6 +90,28 @@ void arrangeWindow(HWND w)
|
|||||||
}
|
}
|
||||||
if (child == layout->bottomLeft || child == layout->bottomRight)
|
if (child == layout->bottomLeft || child == layout->bottomRight)
|
||||||
OffsetRect(&next, child == layout->bottomRight ? extraWidth : 0, extraHeight);
|
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, ¤t);
|
RECT current{}; GetWindowRect(child, ¤t);
|
||||||
MapWindowPoints(nullptr, w, reinterpret_cast<POINT *>(¤t), 2);
|
MapWindowPoints(nullptr, w, reinterpret_cast<POINT *>(¤t), 2);
|
||||||
if (!EqualRect(¤t, &next)) moves.push_back({child, next});
|
if (!EqualRect(¤t, &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,
|
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc,
|
||||||
560, 680);
|
560, 680);
|
||||||
|
windowLayoutState(w)->autoOptions = true;
|
||||||
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
||||||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v});
|
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);
|
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);
|
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"OK", IDOK, 326, 632, 100);
|
||||||
button(w, L"Cancel", IDCANCEL, 440, 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);
|
EnableWindow(parent, FALSE);
|
||||||
showRequestedWindow(w);
|
showRequestedWindow(w);
|
||||||
SetForegroundWindow(w);
|
SetForegroundWindow(w);
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
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 UpdateMessage = WM_APP + 7;
|
||||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||||
|
|||||||
@@ -598,6 +598,35 @@ int main(int argc, char **argv) {
|
|||||||
auto autoOptions=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
|
auto autoOptions=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
|
||||||
require(autoOptions!=nullptr,"auto-correction options open");
|
require(autoOptions!=nullptr,"auto-correction options open");
|
||||||
ShowWindow(autoOptions,SW_HIDE);
|
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}) {
|
for(int id:{411,412,413}) {
|
||||||
RECT control{},client{};
|
RECT control{},client{};
|
||||||
GetWindowRect(GetDlgItem(autoOptions,id),&control);
|
GetWindowRect(GetDlgItem(autoOptions,id),&control);
|
||||||
|
|||||||
@@ -334,7 +334,25 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
stage = 21;
|
stage = 21;
|
||||||
break;
|
break;
|
||||||
case 21:
|
case 21:
|
||||||
|
{
|
||||||
require(text() == L"привет а", "automatic correction preserves immediate next-word typing without a pause");
|
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();
|
finish();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,6 +276,15 @@ int main(int argc, char **argv)
|
|||||||
require(!supplemental.suggest(L"релызик ", 1, layouts, options),
|
require(!supplemental.suggest(L"релызик ", 1, layouts, options),
|
||||||
"never-correct list takes precedence over personal word");
|
"never-correct list takes precedence over personal word");
|
||||||
options.autoNever.clear();
|
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"};
|
options.cycle = {L"en", L"ru"};
|
||||||
require(!supplemental.suggest(L"релызик ", 1, layouts, options),
|
require(!supplemental.suggest(L"релызик ", 1, layouts, options),
|
||||||
"personal dictionary respects disabled target layouts");
|
"personal dictionary respects disabled target layouts");
|
||||||
|
|||||||
Reference in New Issue
Block a user