Release v0.6.21: skip unchanged layouts during conversion
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.20 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.21 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()
|
||||||
|
|||||||
@@ -220,6 +220,8 @@ thumbnails and source labels aligned with the scaled fonts and spacing.
|
|||||||
## Implemented
|
## Implemented
|
||||||
|
|
||||||
- Installed Windows keyboard layouts, configurable cycle/order and direct shortcuts; IMEs are switch-only.
|
- Installed Windows keyboard layouts, configurable cycle/order and direct shortcuts; IMEs are switch-only.
|
||||||
|
Text conversion cycles skip layouts that leave the text unchanged and use the next enabled layout
|
||||||
|
that changes it. If none changes the text, the operation keeps the text and advances one layout.
|
||||||
- Selection conversion and verified recent input (physical scan codes, 2-second typing-run boundary).
|
- Selection conversion and verified recent input (physical scan codes, 2-second typing-run boundary).
|
||||||
- Same-process clipboard ownership plus verified WebView2 descendants, including the Teams fix.
|
- Same-process clipboard ownership plus verified WebView2 descendants, including the Teams fix.
|
||||||
- Teams recent-word conversion uses a single word-selection command and validates the copied range.
|
- Teams recent-word conversion uses a single word-selection command and validates the copied range.
|
||||||
|
|||||||
Binary file not shown.
+5
-5
@@ -1,16 +1,16 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.20 — 2026-10-31
|
## v0.6.21 — 2026-10-31
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Open clipboard-history images in the built-in screenshot editor with **Edit image**, **Ctrl+O**, or **Edit image in screenshot editor** in the context menu. PNG, DIB and DIBV5 open at full resolution through an encrypted local cache, without requiring notifications or changing the clipboard/original history entry. Copy the annotated result explicitly from the editor.
|
- Next layout skips enabled layouts that leave the text unchanged and selects the next layout that changes it. Shared Russian/Ukrainian text such as `рф` and `шььш` becomes `ha` and `immi` when English follows in the cycle.
|
||||||
- Right-clicking a history entry selects that entry before showing its actions. Enter and double-click continue to paste; text items still open in the full-text viewer.
|
- Configured order is preserved. If no enabled convertible layout changes the text, it remains unchanged and the operation advances one layout. Direct layout shortcuts retain their existing behavior.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
Passed 406 native regressions, 473 input/UI checks, screenshot editor UI, notification registration, 48 updater checks and updater progress checks. Manual testing confirmed image editing from history.
|
Passed 413 native regressions, 473 input/UI checks, screenshot editor UI, notification registration, 48 updater checks and updater progress checks.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
Image decoding or a normalized image larger than the 128 MiB editor-transfer limit can prevent opening; an error is shown. Existing clipboard, OCR and editor privilege limitations remain.
|
Existing clipboard, accessibility and input-context verification limitations remain. Copy retries and paste readback verification are unchanged.
|
||||||
|
|
||||||
**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, close screenshot editors and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
For manual installation, exit KSwitcher, close screenshot editors and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0b3c4fd1346d0b4eeb73d276193c5d2789fd32986c3cc75384cf489f23629678 KSwitcher-win-x64.zip
|
5207831872af5522063c1c549ba582aaed534096e6e51a394b6be6a705951f3d 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,20,0
|
FILEVERSION 0,6,21,0
|
||||||
PRODUCTVERSION 0,6,20,0
|
PRODUCTVERSION 0,6,21,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.20"
|
VALUE "FileVersion", "0.6.21"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+1
-5
@@ -1202,11 +1202,7 @@ void App::transform(int command, int snippet)
|
|||||||
chord(VK_RIGHT);
|
chord(VK_RIGHT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int source = detect(text, available, hint);
|
to = nextConversionLayout(text, available, cycle, hint);
|
||||||
if (source < 0)
|
|
||||||
source = hint;
|
|
||||||
auto it = std::find(cycle.begin(), cycle.end(), source);
|
|
||||||
to = it == cycle.end() || ++it == cycle.end() ? cycle.front() : *it;
|
|
||||||
}
|
}
|
||||||
result = convertTo(text, available, to, hint);
|
result = convertTo(text, available, to, hint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,26 @@ std::wstring convertTo(const std::wstring &text, const std::vector<Layout> &layo
|
|||||||
}
|
}
|
||||||
return best < 0 ? text : convert(text, layouts[best], to);
|
return best < 0 ? text : convert(text, layouts[best], to);
|
||||||
}
|
}
|
||||||
|
int nextConversionLayout(const std::wstring &text, const std::vector<Layout> &layouts,
|
||||||
|
const std::vector<int> &cycle, int hint)
|
||||||
|
{
|
||||||
|
if (cycle.empty())
|
||||||
|
return -1;
|
||||||
|
int source = detect(text, layouts, hint);
|
||||||
|
if (source < 0)
|
||||||
|
source = hint;
|
||||||
|
auto it = std::find(cycle.begin(), cycle.end(), source);
|
||||||
|
size_t start = it == cycle.end() ? 0 : (size_t(it - cycle.begin()) + 1) % cycle.size();
|
||||||
|
// Compare every candidate with the original input, never an intermediate conversion.
|
||||||
|
// Bound the search so digits, whitespace and identical layouts cannot loop forever.
|
||||||
|
for (size_t offset = 0; offset < cycle.size(); ++offset)
|
||||||
|
{
|
||||||
|
int target = cycle[(start + offset) % cycle.size()];
|
||||||
|
if (target != source && convertTo(text, layouts, target, hint) != text)
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
return cycle[start];
|
||||||
|
}
|
||||||
std::wstring cycleCase(const std::wstring &text)
|
std::wstring cycleCase(const std::wstring &text)
|
||||||
{
|
{
|
||||||
if (text == upper(text))
|
if (text == upper(text))
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ std::wstring upper(std::wstring text);
|
|||||||
std::wstring convert(const std::wstring &, const Layout &, const Layout &, bool force = false);
|
std::wstring convert(const std::wstring &, const Layout &, const Layout &, bool force = false);
|
||||||
int detect(const std::wstring &, const std::vector<Layout> &, int preferred = -1);
|
int detect(const std::wstring &, const std::vector<Layout> &, int preferred = -1);
|
||||||
std::wstring convertTo(const std::wstring &, const std::vector<Layout> &, int target, int hint);
|
std::wstring convertTo(const std::wstring &, const std::vector<Layout> &, int target, int hint);
|
||||||
|
int nextConversionLayout(const std::wstring &, const std::vector<Layout> &,
|
||||||
|
const std::vector<int> &cycle, int hint);
|
||||||
std::wstring cycleCase(const std::wstring &);
|
std::wstring cycleCase(const std::wstring &);
|
||||||
std::wstring transliterate(const std::wstring &);
|
std::wstring transliterate(const std::wstring &);
|
||||||
struct Snippet
|
struct Snippet
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.20";
|
inline constexpr wchar_t AppVersion[] = L"0.6.21";
|
||||||
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;
|
||||||
|
|||||||
@@ -337,6 +337,22 @@ int main(int argc, char **argv)
|
|||||||
require(ks::convertTo(L"ghbdtn", layouts, 1, 0) == L"привет", "hint conversion");
|
require(ks::convertTo(L"ghbdtn", layouts, 1, 0) == L"привет", "hint conversion");
|
||||||
require(ks::convertTo(L"привет", layouts, 1, 1) == L"привет", "same source is unchanged");
|
require(ks::convertTo(L"привет", layouts, 1, 1) == L"привет", "same source is unchanged");
|
||||||
require(ks::convertTo(L"?", layouts, 1, 0) == L",", "punctuation conversion");
|
require(ks::convertTo(L"?", layouts, 1, 0) == L",", "punctuation conversion");
|
||||||
|
require(ks::nextConversionLayout(L"рф", layouts, {0, 1, 2}, 1) == 0 &&
|
||||||
|
ks::convertTo(L"рф", layouts, 0, 1) == L"ha",
|
||||||
|
"cycle skips unchanged Russian to Ukrainian and wraps to English");
|
||||||
|
require(ks::nextConversionLayout(L"шььш", layouts, {0, 2, 1}, 2) == 0 &&
|
||||||
|
ks::convertTo(L"шььш", layouts, 0, 2) == L"immi",
|
||||||
|
"cycle skips unchanged Ukrainian to Russian");
|
||||||
|
require(ks::nextConversionLayout(L"і", layouts, {0, 2, 1}, 2) == 1,
|
||||||
|
"cycle keeps the next language when text changes");
|
||||||
|
require(ks::nextConversionLayout(L"ha", layouts, {0, 1, 2}, 0) == 1,
|
||||||
|
"cycle preserves configured order for changed text");
|
||||||
|
require(ks::nextConversionLayout(L"рф", layouts, {1, 2}, 1) == 2,
|
||||||
|
"cycle does not use disabled English when all candidates are unchanged");
|
||||||
|
require(ks::nextConversionLayout(L"123 ", layouts, {0, 1, 2}, 0) == 1,
|
||||||
|
"unchanged digits finish after a bounded cycle");
|
||||||
|
require(ks::nextConversionLayout(L"рф", layouts, {2, 0}, 1) == 0,
|
||||||
|
"source outside cycle still skips unchanged candidates");
|
||||||
require(ks::cycleCase(L"hello world") == L"Hello World", "title case");
|
require(ks::cycleCase(L"hello world") == L"Hello World", "title case");
|
||||||
require(ks::cycleCase(L"Hello World") == L"HELLO WORLD", "upper case");
|
require(ks::cycleCase(L"Hello World") == L"HELLO WORLD", "upper case");
|
||||||
require(ks::cycleCase(L"ПРИВІТ") == L"привіт", "Unicode lower case");
|
require(ks::cycleCase(L"ПРИВІТ") == L"привіт", "Unicode lower case");
|
||||||
|
|||||||
Reference in New Issue
Block a user