Release v0.6.23: improve browser selection, Ukrainian correction and PNG saving
This commit is contained in:
+2
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.22 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.23 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()
|
||||||
@@ -61,6 +61,7 @@ add_test(NAME native-regressions COMMAND KSwitcherTests)
|
|||||||
add_test(NAME scroll-regressions COMMAND KSwitcherScrollTests)
|
add_test(NAME scroll-regressions COMMAND KSwitcherScrollTests)
|
||||||
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
|
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
|
||||||
add_test(NAME screenshot-ui-regressions COMMAND KSwitcherScreenshotUITests)
|
add_test(NAME screenshot-ui-regressions COMMAND KSwitcherScreenshotUITests)
|
||||||
|
add_test(NAME screenshot-save-regressions COMMAND KSwitcherScreenshotUITests --save-only)
|
||||||
add_test(NAME updater-regressions COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/update.ps1" -Executable "$<TARGET_FILE:KSwitcher>")
|
add_test(NAME updater-regressions COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/update.ps1" -Executable "$<TARGET_FILE:KSwitcher>")
|
||||||
set_tests_properties(native-regressions PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
|
set_tests_properties(native-regressions PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
install(TARGETS KSwitcher KSwitcherConfig KSwitcherScreenshot RUNTIME DESTINATION .)
|
install(TARGETS KSwitcher KSwitcherConfig KSwitcherScreenshot RUNTIME DESTINATION .)
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ broker can load them even when the profile directory is inaccessible to it.
|
|||||||
Notifications expire after 24 hours. Cache cleanup runs on capture/open; each cache is bounded to
|
Notifications expire after 24 hours. Cache cleanup runs on capture/open; each cache is bounded to
|
||||||
40 files and 256 MiB. Expired or evicted notifications cannot reopen their screenshot.
|
40 files and 256 MiB. Expired or evicted notifications cannot reopen their screenshot.
|
||||||
In the editor, **Ctrl+C** or **Enter** copies the current result without closing the window.
|
In the editor, **Ctrl+C** or **Enter** copies the current result without closing the window.
|
||||||
|
**Save… / Ctrl+S** saves the full-resolution image and annotations as PNG. The first save asks
|
||||||
|
for a file; later saves update that file. **Ctrl+Shift+S (Save As…)** always asks for a new
|
||||||
|
destination. Saving keeps the editor open and leaves the clipboard unchanged. Further edits
|
||||||
|
must be saved or copied again before closing without a warning.
|
||||||
The editor starts with **Pencil / Freehand (F)** for drawing. Choose **Pointer (V)** to select objects, move them, and edit geometry, thickness and
|
The editor starts with **Pencil / Freehand (F)** for drawing. Choose **Pointer (V)** to select objects, move them, and edit geometry, thickness and
|
||||||
other properties using handles and controls. Clicking empty space clears selection; dragging empty
|
other properties using handles and controls. Clicking empty space clears selection; dragging empty
|
||||||
space adds nothing. Choose a drawing tool to create objects, then return to **V** to edit them.
|
space adds nothing. Choose a drawing tool to create objects, then return to **V** to edit them.
|
||||||
@@ -168,7 +172,7 @@ released repeatedly within one stroke. The **Line style** selector supports Soli
|
|||||||
Dash-dot; **Ends** supports an arrow at the end, start, both ends, or no arrows. These settings
|
Dash-dot; **Ends** supports an arrow at the end, start, both ends, or no arrows. These settings
|
||||||
apply to selected straight/freehand lines and set defaults for subsequent lines. Both are disabled
|
apply to selected straight/freehand lines and set defaults for subsequent lines. Both are disabled
|
||||||
for numbered markers and highlights unless a line drawing tool is active. Undo/Redo includes style changes.
|
for numbered markers and highlights unless a line drawing tool is active. Undo/Redo includes style changes.
|
||||||
The window close button asks for confirmation before discarding an uncopied screenshot.
|
The window close button asks for confirmation before discarding changes that have not been saved or copied.
|
||||||
**Escape** cancels region selection or inline text edits; it does not close the editor. Copy keeps the editor open and publishes the image
|
**Escape** cancels region selection or inline text edits; it does not close the editor. Copy keeps the editor open and publishes the image
|
||||||
at its original pixel size, ready to paste. The preview fits the window without changing export size.
|
at its original pixel size, ready to paste. The preview fits the window without changing export size.
|
||||||
The shortcut is configurable in Settings under **Screen capture**.
|
The shortcut is configurable in Settings under **Screen capture**.
|
||||||
@@ -389,6 +393,8 @@ protect already valid input, and ambiguous alternatives remain unchanged regardl
|
|||||||
**My dictionary** provides separate English, Ukrainian and Russian fields for correct words missing
|
**My dictionary** provides separate English, Ukrainian and Russian fields for correct words missing
|
||||||
from the bundled dictionaries. Enter words separated by commas or new lines, then click OK and Save
|
from the bundled dictionaries. Enter words separated by commas or new lines, then click OK and Save
|
||||||
in Settings. Case and duplicates are normalized automatically; no file editing or ranks are needed.
|
in Settings. Case and duplicates are normalized automatically; no file editing or ranks are needed.
|
||||||
|
The v0.6.23 bundled Ukrainian dictionary includes `поточні`. The updater preserves existing dictionary files;
|
||||||
|
if upgrading from an older dictionary, add this word in My dictionary → Ukrainian and click OK → Save.
|
||||||
These entries are stored in settings and survive dictionary updates. They protect correct input and
|
These entries are stored in settings and survive dictionary updates. They protect correct input and
|
||||||
are eligible as correction targets at any frequency limit. The language and target layout must still
|
are eligible as correction targets at any frequency limit. The language and target layout must still
|
||||||
be enabled. **Never correct these words** is a separate exception list and takes precedence.
|
be enabled. **Never correct these words** is a separate exception list and takes precedence.
|
||||||
|
|||||||
Binary file not shown.
+8
-6
@@ -1,17 +1,19 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.22 — 2026-10-31
|
## v0.6.23 — 2026-11-01
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Recover partial selections during recent-text conversion, repeat conversion and undo: when copying returns a verified suffix of the expected text, extend the selection with paced Shift+Left presses and copy again. Replacement still requires a complete verified match.
|
- Add Ukrainian `поточні` to the bundled dictionary: `Gjnjxys ` and `Поточны ` can become `Поточні ` with Ukrainian enabled. Supplemental rank 50001 is included by the default unlimited frequency setting.
|
||||||
- Recent-text suffix checks map physical keys through convertible layouts, including Cyrillic and mixed case. This covers reported suffixes such as `Switcher ` from `PuntoSwitcher `, `зштп ` from the layout equivalent of `snipping `, and `лувШт ` from `LinkedIn `.
|
- Use paced selection in Chrome and Edge, as already used for Teams/WebView, to reduce incomplete selections during recent-text conversion, repeat conversion and Undo. Full verification remains mandatory.
|
||||||
- Diagnostic context records the expected previous result and selection-recovery stages. Unrelated or empty copied ranges are not extended; input/context changes still cancel replacement.
|
- Save annotated screenshots as full-resolution PNG with **Save… / Ctrl+S**. The first save asks for a destination; later saves update it. **Ctrl+Shift+S** opens Save As. Saving keeps the editor open and does not change the clipboard. Closing warns about changes not saved or copied.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
419 native regressions, 473 input/UI checks, screenshot editor UI, notification registration, 48 updater checks and updater progress checks passed, including Cyrillic and mixed-case suffix verification. The isolated editor integration test passed partial-selection recovery, the `PuntoSwitcher ` repeat-conversion case, clipboard restoration and rejection of unrelated text. Direct validation in LinkedIn and ChatGPT is still outstanding; native-editor tests do not establish browser-editor compatibility.
|
423 native regressions, 473 input/UI checks, shared wheel handling, screenshot editor UI, PNG save regressions, notification registration, 48 updater checks and updater progress checks passed.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
Recovery is attempted once and still depends on the editor processing simulated selection keys. A failed full-text check cancels replacement. Existing clipboard, accessibility and input-context restrictions remain; this patch does not change paste readback verification.
|
The reported Chrome editor failure has not yet been verified live after the selection change. Existing clipboard/accessibility restrictions and paste readback limitations remain. Automatic correction runs after Space and requires a dictionary match.
|
||||||
|
|
||||||
|
**Dictionary upgrade:** the updater preserves existing dictionaries, so an older installation may not receive the new word automatically. Add `поточні` under **Settings → Auto-correction options → My dictionary → Ukrainian**, then click **OK → Save**. Personal words work at any frequency rank limit. Back up custom dictionary files before replacing them manually.
|
||||||
|
|
||||||
**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 @@
|
|||||||
b80632cc93abadf2a23921c3275b8ff37d98c92b8b583a9621d0c2b589ed6a35 KSwitcher-win-x64.zip
|
414604b295912ad466c1a6c6b6ff85b032b9e4480bf115c249885cef60817475 KSwitcher-win-x64.zip
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ try {
|
|||||||
if ($LASTEXITCODE) { throw 'Screenshot UI test build failed' }
|
if ($LASTEXITCODE) { throw 'Screenshot UI test build failed' }
|
||||||
& .\build\KSwitcherScreenshotUITests.exe
|
& .\build\KSwitcherScreenshotUITests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Screenshot UI tests failed' }
|
if ($LASTEXITCODE) { throw 'Screenshot UI tests failed' }
|
||||||
|
& .\build\KSwitcherScreenshotUITests.exe --save-only
|
||||||
|
if ($LASTEXITCODE) { throw 'Screenshot save tests failed' }
|
||||||
$registrationTest = Join-Path (Split-Path $executablePath) 'KSwitcherRegistrationTests.exe'
|
$registrationTest = Join-Path (Split-Path $executablePath) 'KSwitcherRegistrationTests.exe'
|
||||||
try {
|
try {
|
||||||
& $compiler @common tests/screenshot_registration.cpp build/core.o build/platform.o @libs -static '-Wl,--gc-sections' -s -o $registrationTest
|
& $compiler @common tests/screenshot_registration.cpp build/core.o build/platform.o @libs -static '-Wl,--gc-sections' -s -o $registrationTest
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ replace counts with original frequency ranks, sort lexically. Reproduce with
|
|||||||
hashes. The lists contain word forms from subtitles, not a complete spelling
|
hashes. The lists contain word forms from subtitles, not a complete spelling
|
||||||
or grammar dictionary. Recognition can miss names, jargon and uncommon forms.
|
or grammar dictionary. Recognition can miss names, jargon and uncommon forms.
|
||||||
|
|
||||||
Supplemental Ukrainian vocabulary: `реліз`. These additions use rank 50001,
|
Supplemental Ukrainian vocabulary: `реліз`, `поточні`. These additions use rank 50001,
|
||||||
after the upstream 50k list, rather than claiming a measured corpus frequency.
|
after the upstream 50k list, rather than claiming a measured corpus frequency.
|
||||||
They are included with the default unlimited rank setting and recorded in sources.json.
|
They are included with the default unlimited rank setting and recorded in sources.json.
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
"uk": {
|
"uk": {
|
||||||
"source": "https://raw.githubusercontent.com/hermitdave/FrequencyWords/525f9b560de45753a5ea01069454e72e9aa541c6/content/2016/uk/uk_50k.txt",
|
"source": "https://raw.githubusercontent.com/hermitdave/FrequencyWords/525f9b560de45753a5ea01069454e72e9aa541c6/content/2016/uk/uk_50k.txt",
|
||||||
"sourceSha256": "9548b79a608d33f39c5546037a69117d679730b1fda08dd0ce67c043dbf3335a",
|
"sourceSha256": "9548b79a608d33f39c5546037a69117d679730b1fda08dd0ce67c043dbf3335a",
|
||||||
"words": 42550,
|
"words": 42551,
|
||||||
"bytes": 898227,
|
"bytes": 898248,
|
||||||
"sha256": "c94605e6be676d2d64f89a3bc16f92b9128f9977d226a97b103cf22a3e6835e3",
|
"sha256": "caeb097749d4afc84b68ad96b3d7dfb6a6569561cd32ccce0bee94a13b43ba86",
|
||||||
"supplementalWords": {
|
"supplementalWords": {
|
||||||
"\u0440\u0435\u043b\u0456\u0437": 50001
|
"\u0440\u0435\u043b\u0456\u0437": 50001,
|
||||||
|
"\u043f\u043e\u0442\u043e\u0447\u043d\u0456": 50001
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
|
|||||||
@@ -27337,6 +27337,7 @@
|
|||||||
поторопись 9062
|
поторопись 9062
|
||||||
поторопитесь 20656
|
поторопитесь 20656
|
||||||
поторопиться 15390
|
поторопиться 15390
|
||||||
|
поточні 50001
|
||||||
потрапив 4168
|
потрапив 4168
|
||||||
потрапила 7383
|
потрапила 7383
|
||||||
потрапили 6631
|
потрапили 6631
|
||||||
|
|||||||
+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,22,0
|
FILEVERSION 0,6,23,0
|
||||||
PRODUCTVERSION 0,6,22,0
|
PRODUCTVERSION 0,6,23,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.22"
|
VALUE "FileVersion", "0.6.23"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+37
@@ -186,6 +186,43 @@ Pixels decodeImage(const Clip &clip, UINT maxWidth, UINT maxHeight)
|
|||||||
check(converter->CopyPixels(nullptr, pixels.width * 4, (UINT)pixels.bgra.size(), pixels.bgra.data()));
|
check(converter->CopyPixels(nullptr, pixels.width * 4, (UINT)pixels.bgra.size(), pixels.bgra.data()));
|
||||||
return pixels;
|
return pixels;
|
||||||
}
|
}
|
||||||
|
Bytes encodePng(const Pixels &p)
|
||||||
|
{
|
||||||
|
if (!p.width || !p.height || uint64_t(p.width) * p.height * 4 != p.bgra.size() ||
|
||||||
|
p.bgra.size() > UINT_MAX || p.width > UINT_MAX / 4)
|
||||||
|
throw std::runtime_error("Invalid PNG image dimensions");
|
||||||
|
Com<IWICImagingFactory> factory;
|
||||||
|
check(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
||||||
|
IID_PPV_ARGS(factory.out())));
|
||||||
|
Com<IStream> stream;
|
||||||
|
check(CreateStreamOnHGlobal(nullptr, TRUE, stream.out()));
|
||||||
|
Com<IWICBitmapEncoder> encoder;
|
||||||
|
check(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.out()));
|
||||||
|
check(encoder->Initialize(stream.p, WICBitmapEncoderNoCache));
|
||||||
|
Com<IWICBitmapFrameEncode> frame;
|
||||||
|
check(encoder->CreateNewFrame(frame.out(), nullptr));
|
||||||
|
check(frame->Initialize(nullptr));
|
||||||
|
check(frame->SetSize(p.width, p.height));
|
||||||
|
auto format = GUID_WICPixelFormat32bppBGRA;
|
||||||
|
check(frame->SetPixelFormat(&format));
|
||||||
|
if (format != GUID_WICPixelFormat32bppBGRA)
|
||||||
|
throw std::runtime_error("Unsupported PNG pixel format");
|
||||||
|
check(frame->WritePixels(p.height, p.width * 4, (UINT)p.bgra.size(),
|
||||||
|
const_cast<BYTE *>(p.bgra.data())));
|
||||||
|
check(frame->Commit());
|
||||||
|
check(encoder->Commit());
|
||||||
|
STATSTG stat{};
|
||||||
|
check(stream->Stat(&stat, STATFLAG_NONAME));
|
||||||
|
if (stat.cbSize.QuadPart > UINT_MAX)
|
||||||
|
throw std::runtime_error("PNG image is too large");
|
||||||
|
check(stream->Seek({}, STREAM_SEEK_SET, nullptr));
|
||||||
|
Bytes bytes((size_t)stat.cbSize.QuadPart);
|
||||||
|
ULONG read{};
|
||||||
|
check(stream->Read(bytes.data(), (ULONG)bytes.size(), &read));
|
||||||
|
if (read != bytes.size())
|
||||||
|
throw std::runtime_error("Cannot read encoded PNG image");
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
Format imageDib(const Pixels &p)
|
Format imageDib(const Pixels &p)
|
||||||
{
|
{
|
||||||
BITMAPINFOHEADER h{};
|
BITMAPINFOHEADER h{};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ struct Pixels
|
|||||||
};
|
};
|
||||||
Pixels decodeImage(const Clip &, UINT maxWidth = 0, UINT maxHeight = 0);
|
Pixels decodeImage(const Clip &, UINT maxWidth = 0, UINT maxHeight = 0);
|
||||||
Format imageDib(const Pixels &);
|
Format imageDib(const Pixels &);
|
||||||
|
Bytes encodePng(const Pixels &);
|
||||||
std::wstring recognizeText(const Clip &);
|
std::wstring recognizeText(const Clip &);
|
||||||
std::wstring recognizeText(const Clip &, const Settings &);
|
std::wstring recognizeText(const Clip &, const Settings &);
|
||||||
std::filesystem::path tesseractExecutable(const Settings &);
|
std::filesystem::path tesseractExecutable(const Settings &);
|
||||||
|
|||||||
+3
-2
@@ -396,13 +396,14 @@ bool chord(WORD key, UINT mods)
|
|||||||
bool needsSelectionDelay(HWND target)
|
bool needsSelectionDelay(HWND target)
|
||||||
{
|
{
|
||||||
const auto name = lower(processName(target));
|
const auto name = lower(processName(target));
|
||||||
return name == L"ms-teams" || name == L"msedgewebview2";
|
return name == L"ms-teams" || name == L"msedgewebview2" ||
|
||||||
|
name == L"chrome" || name == L"msedge";
|
||||||
}
|
}
|
||||||
bool selectPrevious(size_t count, const std::function<bool()> &valid)
|
bool selectPrevious(size_t count, const std::function<bool()> &valid)
|
||||||
{
|
{
|
||||||
if (count > 100000)
|
if (count > 100000)
|
||||||
return false;
|
return false;
|
||||||
// Teams' WebView editor applies selection changes asynchronously. A burst of
|
// Chromium and Teams' WebView editors apply selection changes asynchronously. A burst of
|
||||||
// Shift+Left events can all use the same caret position and select only one
|
// Shift+Left events can all use the same caret position and select only one
|
||||||
// character. Give each change time to settle before sending the next one.
|
// character. Give each change time to settle before sending the next one.
|
||||||
const auto target = GetForegroundWindow();
|
const auto target = GetForegroundWindow();
|
||||||
|
|||||||
+78
-2
@@ -28,8 +28,8 @@ bool confirmScreenshotClose(HWND owner)
|
|||||||
dialog.hMainIcon=icon;
|
dialog.hMainIcon=icon;
|
||||||
dialog.dwCommonButtons=TDCBF_YES_BUTTON|TDCBF_NO_BUTTON;
|
dialog.dwCommonButtons=TDCBF_YES_BUTTON|TDCBF_NO_BUTTON;
|
||||||
dialog.pszWindowTitle=L"KSwitcher";
|
dialog.pszWindowTitle=L"KSwitcher";
|
||||||
dialog.pszMainInstruction=L"Close without copying?";
|
dialog.pszMainInstruction=L"Close without saving or copying?";
|
||||||
dialog.pszContent=L"Your latest screenshot changes have not been copied to the clipboard.";
|
dialog.pszContent=L"Your latest screenshot changes have not been saved to a file or copied to the clipboard.";
|
||||||
dialog.nDefaultButton=IDNO;
|
dialog.nDefaultButton=IDNO;
|
||||||
dialog.lpCallbackData=(LONG_PTR)owner;
|
dialog.lpCallbackData=(LONG_PTR)owner;
|
||||||
dialog.pfCallback=[](HWND window,UINT notification,WPARAM,LPARAM,LONG_PTR data)->HRESULT {
|
dialog.pfCallback=[](HWND window,UINT notification,WPARAM,LPARAM,LONG_PTR data)->HRESULT {
|
||||||
@@ -208,6 +208,7 @@ struct Capture
|
|||||||
int hoveredTool=0;
|
int hoveredTool=0;
|
||||||
MarkPart hoveredPart=MarkPart::None;
|
MarkPart hoveredPart=MarkPart::None;
|
||||||
std::wstring statusText;
|
std::wstring statusText;
|
||||||
|
std::filesystem::path savedPath;
|
||||||
explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {}
|
explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {}
|
||||||
~Capture() {
|
~Capture() {
|
||||||
if(pencilCursor) DestroyCursor(pencilCursor);
|
if(pencilCursor) DestroyCursor(pencilCursor);
|
||||||
@@ -239,6 +240,7 @@ void updateStatus(Capture &c)
|
|||||||
if(c.hoveredTool==30) text=L"Line style: solid, dotted, dashed or dash-dot";
|
if(c.hoveredTool==30) text=L"Line style: solid, dotted, dashed or dash-dot";
|
||||||
if(c.hoveredTool==31) text=L"Line ends: start, end, both or no arrowheads";
|
if(c.hoveredTool==31) text=L"Line ends: start, end, both or no arrowheads";
|
||||||
if(c.hoveredTool==35) text=L"Arrow caption: F2 / double-click to edit; drag to reposition";
|
if(c.hoveredTool==35) text=L"Arrow caption: F2 / double-click to edit; drag to reposition";
|
||||||
|
if(c.hoveredTool==28) text=L"Save (Ctrl+S) | Save As... (Ctrl+Shift+S)";
|
||||||
}
|
}
|
||||||
else if(c.inlineText) text=L"Text: Enter = new line | Ctrl+Enter = apply | Esc = cancel";
|
else if(c.inlineText) text=L"Text: Enter = new line | Ctrl+Enter = apply | Esc = cancel";
|
||||||
else if(part==MarkPart::Rotation) text=L"Drag to rotate text | Hold Shift for 15-degree steps";
|
else if(part==MarkPart::Rotation) text=L"Drag to rotate text | Hold Shift for 15-degree steps";
|
||||||
@@ -337,6 +339,10 @@ void finishInlineText(HWND w,Capture &c,bool commit)
|
|||||||
}
|
}
|
||||||
LRESULT CALLBACK inlineTextProc(HWND w,UINT msg,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
|
LRESULT CALLBACK inlineTextProc(HWND w,UINT msg,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
|
||||||
{
|
{
|
||||||
|
if(msg==WM_KEYDOWN && wp=='S' && (GetKeyState(VK_CONTROL)&0x8000)) {
|
||||||
|
PostMessageW(GetParent(w),WM_COMMAND,(GetKeyState(VK_SHIFT)&0x8000)?29:28,0); return 0;
|
||||||
|
}
|
||||||
|
if(msg==WM_CHAR && wp==19) return 0; // Ctrl+S is handled by the editor.
|
||||||
if(msg==WM_KEYDOWN && (wp==VK_ESCAPE || (wp==VK_RETURN && (GetKeyState(VK_CONTROL)&0x8000)))) {
|
if(msg==WM_KEYDOWN && (wp==VK_ESCAPE || (wp==VK_RETURN && (GetKeyState(VK_CONTROL)&0x8000)))) {
|
||||||
PostMessageW(GetParent(w),FinishTextMessage,wp==VK_ESCAPE?2:1,(LPARAM)w); return 0;
|
PostMessageW(GetParent(w),FinishTextMessage,wp==VK_ESCAPE?2:1,(LPARAM)w); return 0;
|
||||||
}
|
}
|
||||||
@@ -607,6 +613,9 @@ void layout(HWND w, Capture &c)
|
|||||||
MoveWindow(GetDlgItem(w,ToolIds[i]),MulDiv(positions[i],dpi,96),MulDiv(6,dpi,96),
|
MoveWindow(GetDlgItem(w,ToolIds[i]),MulDiv(positions[i],dpi,96),MulDiv(6,dpi,96),
|
||||||
MulDiv(32,dpi,96),MulDiv(32,dpi,96),TRUE);
|
MulDiv(32,dpi,96),MulDiv(32,dpi,96),TRUE);
|
||||||
}
|
}
|
||||||
|
SendDlgItemMessageW(w,28,WM_SETFONT,(WPARAM)c.font,TRUE);
|
||||||
|
MoveWindow(GetDlgItem(w,28),MulDiv(540,dpi,96),MulDiv(6,dpi,96),
|
||||||
|
MulDiv(78,dpi,96),MulDiv(32,dpi,96),TRUE);
|
||||||
c.panelHeight=c.unit+MulDiv(40,dpi,96);
|
c.panelHeight=c.unit+MulDiv(40,dpi,96);
|
||||||
for(int id:{30,31,32,33,34,35}) SendDlgItemMessageW(w,id,WM_SETFONT,(WPARAM)c.font,TRUE);
|
for(int id:{30,31,32,33,34,35}) SendDlgItemMessageW(w,id,WM_SETFONT,(WPARAM)c.font,TRUE);
|
||||||
MoveWindow(GetDlgItem(w,32),MulDiv(12,dpi,96),c.unit+MulDiv(6,dpi,96),MulDiv(64,dpi,96),MulDiv(24,dpi,96),TRUE);
|
MoveWindow(GetDlgItem(w,32),MulDiv(12,dpi,96),c.unit+MulDiv(6,dpi,96),MulDiv(64,dpi,96),MulDiv(24,dpi,96),TRUE);
|
||||||
@@ -821,6 +830,64 @@ void copy(HWND w, Capture &c, DWORD (*publish)(HWND,const Clip &)=writeClipboard
|
|||||||
publish(c.app.window, clip);
|
publish(c.app.window, clip);
|
||||||
c.copied=true;
|
c.copied=true;
|
||||||
}
|
}
|
||||||
|
void saveToFile(HWND w, Capture &c, const std::filesystem::path &path)
|
||||||
|
{
|
||||||
|
finishInlineText(w,c,true);
|
||||||
|
auto pixels=decodeImage(screenshotClip(c.image->dc,c.image->width,c.image->height,c.marks));
|
||||||
|
// GDI-rendered screenshot pixels are opaque; GDI does not maintain alpha.
|
||||||
|
for(size_t i=3;i<pixels.bgra.size();i+=4) pixels.bgra[i]=255;
|
||||||
|
auto bytes=encodePng(pixels);
|
||||||
|
GUID id{};
|
||||||
|
if(FAILED(CoCreateGuid(&id))) throw std::runtime_error("Cannot prepare screenshot output file");
|
||||||
|
wchar_t unique[40]{}; StringFromGUID2(id,unique,(int)std::size(unique));
|
||||||
|
auto temporary=path.parent_path()/(L".kswitcher-"+std::wstring(unique)+L".png");
|
||||||
|
try {
|
||||||
|
atomicWrite(temporary,bytes);
|
||||||
|
if(!MoveFileExW(temporary.c_str(),path.c_str(),MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH))
|
||||||
|
throw std::runtime_error("Cannot save screenshot to the selected file");
|
||||||
|
} catch(...) {
|
||||||
|
std::error_code ignored; std::filesystem::remove(temporary,ignored); throw;
|
||||||
|
}
|
||||||
|
c.savedPath=path;
|
||||||
|
c.copied=true; // Current edits have been exported, either to file or clipboard.
|
||||||
|
}
|
||||||
|
void save(HWND w, Capture &c, bool saveAs=false,
|
||||||
|
BOOL (WINAPI *chooseFile)(LPOPENFILENAMEW)=GetSaveFileNameW)
|
||||||
|
{
|
||||||
|
if(!saveAs && !c.savedPath.empty()) {
|
||||||
|
saveToFile(w,c,c.savedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wchar_t filename[32768]{};
|
||||||
|
auto proposed=c.savedPath.wstring();
|
||||||
|
if(proposed.empty()) {
|
||||||
|
SYSTEMTIME time{}; GetLocalTime(&time);
|
||||||
|
wchar_t name[100]{};
|
||||||
|
swprintf_s(name,L"Screenshot-%04u%02u%02u-%02u%02u%02u.png",
|
||||||
|
time.wYear,time.wMonth,time.wDay,time.wHour,time.wMinute,time.wSecond);
|
||||||
|
proposed=name;
|
||||||
|
}
|
||||||
|
if(proposed.size()>=std::size(filename)) throw std::runtime_error("Screenshot file path is too long");
|
||||||
|
std::copy(proposed.begin(),proposed.end(),filename);
|
||||||
|
OPENFILENAMEW dialog{sizeof(dialog)};
|
||||||
|
dialog.hwndOwner=w;
|
||||||
|
dialog.lpstrTitle=L"Save screenshot";
|
||||||
|
dialog.lpstrFilter=L"PNG image (*.png)\0*.png\0\0";
|
||||||
|
dialog.nFilterIndex=1;
|
||||||
|
dialog.lpstrFile=filename;
|
||||||
|
dialog.nMaxFile=(DWORD)std::size(filename);
|
||||||
|
dialog.lpstrDefExt=L"png";
|
||||||
|
dialog.Flags=OFN_OVERWRITEPROMPT|OFN_PATHMUSTEXIST|OFN_NOCHANGEDIR|OFN_EXPLORER;
|
||||||
|
if(!chooseFile(&dialog)) {
|
||||||
|
if(CommDlgExtendedError()) throw std::runtime_error("Cannot open the screenshot save dialog");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(lower(std::filesystem::path(filename).extension().wstring())!=L".png")
|
||||||
|
throw std::runtime_error("Choose a filename ending in .png for the screenshot");
|
||||||
|
saveToFile(w,c,filename);
|
||||||
|
c.statusText=L"Saved: "+c.savedPath.wstring();
|
||||||
|
SendDlgItemMessageW(w,50,SB_SETTEXTW,0,(LPARAM)c.statusText.c_str());
|
||||||
|
}
|
||||||
void edit(HWND w, Capture &c)
|
void edit(HWND w, Capture &c)
|
||||||
{
|
{
|
||||||
auto r = region(c.start,c.end);
|
auto r = region(c.start,c.end);
|
||||||
@@ -849,6 +916,9 @@ void edit(HWND w, Capture &c)
|
|||||||
const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Shift for 45-degree steps; move >50 px off line to turn; Ctrl to draw over objects", L"Highlight (H); Ctrl+drag to paint over an object",
|
const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Shift for 45-degree steps; move >50 px off line to turn; Ctrl to draw over objects", L"Highlight (H); Ctrl+drag to paint over an object",
|
||||||
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
|
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
|
||||||
static_assert(std::size(labels)==std::size(ToolIds));
|
static_assert(std::size(labels)==std::size(ToolIds));
|
||||||
|
auto saveButton=CreateWindowW(L"BUTTON",L"Save...",WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON,
|
||||||
|
0,0,0,0,w,(HMENU)28,c.app.instance,nullptr);
|
||||||
|
SetWindowSubclass(saveButton,toolHover,1,0);
|
||||||
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
|
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
|
||||||
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,w,nullptr,c.app.instance,nullptr);
|
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,w,nullptr,c.app.instance,nullptr);
|
||||||
for (size_t i=0;i<std::size(ToolIds);++i)
|
for (size_t i=0;i<std::size(ToolIds);++i)
|
||||||
@@ -1023,6 +1093,9 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if(next!=value) { c->remember(c->marks); value=next; resolveScreenshotLinks(c->marks); }
|
if(next!=value) { c->remember(c->marks); value=next; resolveScreenshotLinks(c->marks); }
|
||||||
}
|
}
|
||||||
if (LOWORD(wp)==13) { copy(w,*c); return 0; }
|
if (LOWORD(wp)==13) { copy(w,*c); return 0; }
|
||||||
|
if (LOWORD(wp)==28 || LOWORD(wp)==29) {
|
||||||
|
save(w,*c,LOWORD(wp)==29); SetFocus(w); return 0;
|
||||||
|
}
|
||||||
if (LOWORD(wp)==12 || LOWORD(wp)==18)
|
if (LOWORD(wp)==12 || LOWORD(wp)==18)
|
||||||
{
|
{
|
||||||
bool changed=LOWORD(wp)==12?c->history.undo(c->marks):c->history.redo(c->marks);
|
bool changed=LOWORD(wp)==12?c->history.undo(c->marks):c->history.redo(c->marks);
|
||||||
@@ -1079,6 +1152,9 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
beginMarkText(w,*c,c->selected); return 0;
|
beginMarkText(w,*c,c->selected); return 0;
|
||||||
}
|
}
|
||||||
if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='C') { copy(w,*c); return 0; }
|
if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='C') { copy(w,*c); return 0; }
|
||||||
|
if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='S') {
|
||||||
|
save(w,*c,(GetKeyState(VK_SHIFT)&0x8000)!=0); SetFocus(w); return 0;
|
||||||
|
}
|
||||||
if ((GetKeyState(VK_CONTROL)&0x8000) && (wp=='Z' || wp=='Y'))
|
if ((GetKeyState(VK_CONTROL)&0x8000) && (wp=='Z' || wp=='Y'))
|
||||||
SendMessageW(w,WM_COMMAND,wp=='Y'||(GetKeyState(VK_SHIFT)&0x8000)?18:12,0);
|
SendMessageW(w,WM_COMMAND,wp=='Y'||(GetKeyState(VK_SHIFT)&0x8000)?18:12,0);
|
||||||
if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='D' && c->selected>=0)
|
if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='D' && c->selected>=0)
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.22";
|
inline constexpr wchar_t AppVersion[] = L"0.6.23";
|
||||||
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;
|
||||||
|
|||||||
+58
-1
@@ -2,10 +2,67 @@
|
|||||||
// Including the implementation lets this integration test drive private editor state directly.
|
// Including the implementation lets this integration test drive private editor state directly.
|
||||||
#include "../src/screenshot.cpp"
|
#include "../src/screenshot.cpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
int main()
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
||||||
try {
|
try {
|
||||||
|
if(argc>1 && std::string(argv[1])=="--save-only") {
|
||||||
|
auto require=[](bool ok,const char *message){if(!ok) throw std::runtime_error(message);};
|
||||||
|
auto root=std::filesystem::temp_directory_path()/(L"KSwitcher-save-"+std::to_wstring(GetCurrentProcessId()));
|
||||||
|
std::filesystem::create_directories(root);
|
||||||
|
struct Cleanup {std::filesystem::path root; ~Cleanup(){std::error_code ec; std::filesystem::remove_all(root,ec);}} cleanup{root};
|
||||||
|
ks::ScreenshotHost app{GetModuleHandleW(nullptr)};
|
||||||
|
ks::Capture c(app);
|
||||||
|
c.image=std::make_unique<ks::Surface>(320,240);
|
||||||
|
PatBlt(c.image->dc,0,0,320,240,WHITENESS);
|
||||||
|
c.marks={ks::Mark{{40,40},{180,140},0}};
|
||||||
|
auto expected=ks::decodeImage(ks::screenshotClip(c.image->dc,320,240,c.marks));
|
||||||
|
for(size_t i=3;i<expected.bgra.size();i+=4) expected.bgra[i]=255;
|
||||||
|
auto path=root/L"Знімок.png";
|
||||||
|
const auto clipboard=GetClipboardSequenceNumber();
|
||||||
|
ks::saveToFile(nullptr,c,path);
|
||||||
|
ks::Clip encoded{{{0,L"PNG",ks::readFile(path,16*1024*1024)}}};
|
||||||
|
auto actual=ks::decodeImage(encoded);
|
||||||
|
require(actual.width==320 && actual.height==240 && actual.bgra==expected.bgra,
|
||||||
|
"PNG export preserves full resolution, annotations and opaque pixels");
|
||||||
|
require(c.savedPath==path && c.copied,"successful save records destination and exported state");
|
||||||
|
require(GetClipboardSequenceNumber()==clipboard,"saving does not change clipboard");
|
||||||
|
c.remember(c.marks); c.marks.clear();
|
||||||
|
require(!c.copied,"editing after save marks changes unexported");
|
||||||
|
ks::save(nullptr,c);
|
||||||
|
actual=ks::decodeImage(ks::Clip{{{0,L"PNG",ks::readFile(path,16*1024*1024)}}});
|
||||||
|
require(actual.bgra!=expected.bgra && c.copied,"Save overwrites remembered path with current edits without a dialog");
|
||||||
|
c.copied=false;
|
||||||
|
ks::save(nullptr,c,true,[](LPOPENFILENAMEW dialog)->BOOL {
|
||||||
|
if(!(dialog->Flags&OFN_OVERWRITEPROMPT) || std::wstring(dialog->lpstrDefExt)!=L"png" ||
|
||||||
|
!std::wstring(dialog->lpstrFile).ends_with(L"Знімок.png"))
|
||||||
|
throw std::runtime_error("Save As dialog must preserve filename, PNG extension and overwrite confirmation");
|
||||||
|
return FALSE;
|
||||||
|
});
|
||||||
|
require(!c.copied && c.savedPath==path,"cancelled Save As preserves dirty state and remembered path");
|
||||||
|
auto another=root/L"Another.png";
|
||||||
|
ks::save(nullptr,c,true,[](LPOPENFILENAMEW dialog)->BOOL {
|
||||||
|
auto destination=std::filesystem::path(dialog->lpstrFile).parent_path()/L"Another.png";
|
||||||
|
wcscpy_s(dialog->lpstrFile,dialog->nMaxFile,destination.c_str());
|
||||||
|
return TRUE;
|
||||||
|
});
|
||||||
|
require(c.savedPath==another && std::filesystem::exists(path),"new destination preserves previous file");
|
||||||
|
c.copied=false;
|
||||||
|
bool failed=false;
|
||||||
|
try {ks::saveToFile(nullptr,c,root/L"missing"/L"fail.png");} catch(const std::exception &){failed=true;}
|
||||||
|
require(failed && !c.copied && c.savedPath==another,"failed save preserves dirty state and previous destination");
|
||||||
|
require(std::distance(std::filesystem::directory_iterator(root),std::filesystem::directory_iterator{})==2,
|
||||||
|
"saving leaves no temporary files");
|
||||||
|
bool invalid=false;
|
||||||
|
try {ks::save(nullptr,c,true,[](LPOPENFILENAMEW dialog)->BOOL {
|
||||||
|
auto destination=std::filesystem::path(dialog->lpstrFile).parent_path()/L"Wrong.jpg";
|
||||||
|
wcscpy_s(dialog->lpstrFile,dialog->nMaxFile,destination.c_str()); return TRUE;
|
||||||
|
});} catch(const std::exception &){invalid=true;}
|
||||||
|
require(invalid && !std::filesystem::exists(root/L"Wrong.jpg"),"PNG export rejects a misleading file extension");
|
||||||
|
CoUninitialize();
|
||||||
|
std::cout<<"Passed screenshot PNG save regressions without opening windows\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES}; InitCommonControlsEx(&controls);
|
INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES}; InitCommonControlsEx(&controls);
|
||||||
ks::ScreenshotHost app{GetModuleHandleW(nullptr)};
|
ks::ScreenshotHost app{GetModuleHandleW(nullptr)};
|
||||||
app.window=CreateWindowW(L"STATIC",L"",0,0,0,0,0,HWND_MESSAGE,nullptr,app.instance,nullptr);
|
app.window=CreateWindowW(L"STATIC",L"",0,0,0,0,0,HWND_MESSAGE,nullptr,app.instance,nullptr);
|
||||||
|
|||||||
@@ -311,6 +311,14 @@ int main(int argc, char **argv)
|
|||||||
options.autoLanguages = {L"en", L"uk", L"ru"};
|
options.autoLanguages = {L"en", L"uk", L"ru"};
|
||||||
options.cycle = {L"en", L"uk", L"ru"};
|
options.cycle = {L"en", L"uk", L"ru"};
|
||||||
supplemental.configure(L"dictionaries", options.autoLanguages);
|
supplemental.configure(L"dictionaries", options.autoLanguages);
|
||||||
|
auto current = supplemental.suggest(L"Gjnjxys ", 0, layouts, options);
|
||||||
|
require(current && current->text == L"Поточні " && current->target == 2,
|
||||||
|
"reported title-case Ukrainian word corrects with all three dictionaries enabled");
|
||||||
|
current = supplemental.suggest(L"Поточны ", 1, layouts, options);
|
||||||
|
require(current && current->text == L"Поточні " && current->target == 2,
|
||||||
|
"reported Ukrainian word corrects from Russian layout");
|
||||||
|
require(!supplemental.suggest(L"Поточні ", 2, layouts, options),
|
||||||
|
"correct Ukrainian word remains unchanged");
|
||||||
for (auto [typed, expected] : {std::pair{L"ntcn ", L"тест "}, std::pair{L"vfkj ", L"мало "}})
|
for (auto [typed, expected] : {std::pair{L"ntcn ", L"тест "}, std::pair{L"vfkj ", L"мало "}})
|
||||||
{
|
{
|
||||||
auto shared = supplemental.suggest(typed, 0, layouts, options);
|
auto shared = supplemental.suggest(typed, 0, layouts, options);
|
||||||
@@ -327,6 +335,8 @@ int main(int argc, char **argv)
|
|||||||
require(!supplemental.suggest(L"релиз ", 1, layouts, options),
|
require(!supplemental.suggest(L"релиз ", 1, layouts, options),
|
||||||
"valid Russian release word stays unchanged");
|
"valid Russian release word stays unchanged");
|
||||||
options.autoMaxRank = 50000;
|
options.autoMaxRank = 50000;
|
||||||
|
require(!supplemental.suggest(L"Gjnjxys ", 0, layouts, options),
|
||||||
|
"supplemental current word respects explicit frequency limit");
|
||||||
require(!supplemental.suggest(L"релыз ", 1, layouts, options),
|
require(!supplemental.suggest(L"релыз ", 1, layouts, options),
|
||||||
"supplemental vocabulary respects explicit frequency limit");
|
"supplemental vocabulary respects explicit frequency limit");
|
||||||
options.autoWords[L"uk"] = {L"релізик"};
|
options.autoWords[L"uk"] = {L"релізик"};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import urllib.request, hashlib, json
|
|||||||
ROOT=Path(__file__).resolve().parents[1]/'dictionaries'
|
ROOT=Path(__file__).resolve().parents[1]/'dictionaries'
|
||||||
REV='525f9b560de45753a5ea01069454e72e9aa541c6'
|
REV='525f9b560de45753a5ea01069454e72e9aa541c6'
|
||||||
# Supplemental vocabulary has no corpus frequency: rank after the 50k source list.
|
# Supplemental vocabulary has no corpus frequency: rank after the 50k source list.
|
||||||
SUPPLEMENTAL={'uk': {'реліз': 50001}}
|
SUPPLEMENTAL={'uk': {'реліз': 50001, 'поточні': 50001}}
|
||||||
ROOT.mkdir(exist_ok=True)
|
ROOT.mkdir(exist_ok=True)
|
||||||
manifest={}
|
manifest={}
|
||||||
for lang in ('en','uk','ru'):
|
for lang in ('en','uk','ru'):
|
||||||
|
|||||||
Reference in New Issue
Block a user