Release v0.4.0: add screen capture and annotation editor
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.3.0 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.4.0 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()
|
||||||
@@ -9,7 +9,7 @@ add_library(kscore src/core.cpp src/platform.cpp src/autostart.cpp src/autocorre
|
|||||||
target_include_directories(kscore PUBLIC src PRIVATE vendor)
|
target_include_directories(kscore PUBLIC src PRIVATE vendor)
|
||||||
target_compile_definitions(kscore PUBLIC UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX _WIN32_WINNT=0x0A00)
|
target_compile_definitions(kscore PUBLIC UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX _WIN32_WINNT=0x0A00)
|
||||||
target_link_libraries(kscore PUBLIC user32 shell32 advapi32 crypt32 imm32 comctl32 gdi32 ole32 oleaut32 taskschd comdlg32 uuid windowscodecs runtimeobject uiautomationcore)
|
target_link_libraries(kscore PUBLIC user32 shell32 advapi32 crypt32 imm32 comctl32 gdi32 ole32 oleaut32 taskschd comdlg32 uuid windowscodecs runtimeobject uiautomationcore)
|
||||||
add_executable(KSwitcher WIN32 src/main.cpp src/app.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcher WIN32 src/main.cpp src/app.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
target_compile_definitions(KSwitcher PRIVATE KS_RESIDENT_APP)
|
target_compile_definitions(KSwitcher PRIVATE KS_RESIDENT_APP)
|
||||||
target_link_libraries(KSwitcher PRIVATE kscore)
|
target_link_libraries(KSwitcher PRIVATE kscore)
|
||||||
add_executable(KSwitcherConfig WIN32 src/config_main.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcherConfig WIN32 src/config_main.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
@@ -18,10 +18,18 @@ target_compile_definitions(KSwitcherConfig PRIVATE KS_CONFIG_APP)
|
|||||||
target_link_libraries(KSwitcherConfig PRIVATE kscore)
|
target_link_libraries(KSwitcherConfig PRIVATE kscore)
|
||||||
add_executable(KSwitcherTests tests/tests.cpp)
|
add_executable(KSwitcherTests tests/tests.cpp)
|
||||||
target_link_libraries(KSwitcherTests PRIVATE kscore)
|
target_link_libraries(KSwitcherTests PRIVATE kscore)
|
||||||
add_executable(KSwitcherIntegration tests/integration.cpp src/app.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcherIntegration tests/integration.cpp src/app.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
target_link_libraries(KSwitcherIntegration PRIVATE kscore)
|
target_link_libraries(KSwitcherIntegration PRIVATE kscore)
|
||||||
add_executable(KSwitcherInputTests tests/input.cpp src/app.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcherInputTests tests/input.cpp src/app.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
target_link_libraries(KSwitcherInputTests PRIVATE kscore)
|
target_link_libraries(KSwitcherInputTests PRIVATE kscore)
|
||||||
|
add_executable(KSwitcherScreenshotUITests tests/screenshot_ui.cpp src/app.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
|
target_link_libraries(KSwitcherScreenshotUITests PRIVATE kscore)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(KSwitcherScreenshotUITests PRIVATE /utf-8)
|
||||||
|
set_property(TARGET KSwitcherScreenshotUITests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||||
|
else()
|
||||||
|
target_link_options(KSwitcherScreenshotUITests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
|
endif()
|
||||||
add_executable(KSwitcherOcrTests tests/ocr.cpp)
|
add_executable(KSwitcherOcrTests tests/ocr.cpp)
|
||||||
target_link_libraries(KSwitcherOcrTests PRIVATE kscore)
|
target_link_libraries(KSwitcherOcrTests PRIVATE kscore)
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -40,6 +48,7 @@ endif()
|
|||||||
enable_testing()
|
enable_testing()
|
||||||
add_test(NAME native-regressions COMMAND KSwitcherTests)
|
add_test(NAME native-regressions COMMAND KSwitcherTests)
|
||||||
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 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 RUNTIME DESTINATION .)
|
install(TARGETS KSwitcher KSwitcherConfig RUNTIME DESTINATION .)
|
||||||
|
|||||||
@@ -19,3 +19,50 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
Third-party toolbar icons: Lucide (https://lucide.dev)
|
||||||
|
|
||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Lucide Icons and Contributors
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The following Lucide icons are derived from the Feather project:
|
||||||
|
|
||||||
|
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
||||||
|
|
||||||
|
The MIT License (MIT) (for the icons listed above)
|
||||||
|
|
||||||
|
Copyright (c) 2013-present Cole Bemis
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|||||||
@@ -53,6 +53,78 @@ keys cannot trigger recursive commands or overwrite the recorded typing run. Oth
|
|||||||
is accepted, including key-up events and VK-only keyboard events. Unicode packets do not provide
|
is accepted, including key-up events and VK-only keyboard events. Unicode packets do not provide
|
||||||
physical key positions and therefore reset recent-word tracking; selection conversion still works.
|
physical key positions and therefore reset recent-word tracking; selection conversion still works.
|
||||||
|
|
||||||
|
### Screen capture and annotations
|
||||||
|
|
||||||
|
Press **Win+Shift+S**, choose **Capture screen region…** in the tray menu, or run
|
||||||
|
`KSwitcher.exe --capture`. Drag a rectangle on the dimmed, frozen desktop; the selected area stays
|
||||||
|
at normal brightness. Escape cancels. Selection and annotation frames are buffered to avoid flicker.
|
||||||
|
The editor opens with the selected area. Press **Ctrl+C** or **Enter** to copy it immediately,
|
||||||
|
or annotate first: **N** places sequential numbered red markers with a click, and **A** draws
|
||||||
|
red arrows by dragging from the tail to the tip. Click an existing marker or arrow to select it;
|
||||||
|
drag its body to move it. Drag the square handle on a number's right edge to change its diameter
|
||||||
|
(the text scales with it), or either endpoint handle of an arrow to change its length and direction.
|
||||||
|
Select an arrow and drag the extra handle beside its midpoint perpendicular to the line to change
|
||||||
|
its thickness from 1 to 30 pixels. The arrowhead scales with the line. Minus/plus keys also work.
|
||||||
|
Alternatively, scroll the mouse wheel over an arrow to change its thickness, or over a number to
|
||||||
|
change its diameter. Wheel up increases the size; wheel down decreases it. These edits support Undo
|
||||||
|
and update linked arrows. **Ctrl+D** clones the selected object with a small offset. A cloned number
|
||||||
|
gets the smallest available positive number, just like a newly placed number: gaps left by deletion
|
||||||
|
are filled first, without renumbering existing objects. A cloned arrow keeps its style but starts without endpoint links.
|
||||||
|
The compact toolbar uses [Lucide](https://lucide.dev) icons with hover tooltips showing each action
|
||||||
|
and its keyboard shortcut.
|
||||||
|
Drop either arrow endpoint onto a number to link it to the circle's edge. Linked endpoints follow
|
||||||
|
the number when it moves or changes diameter. Drag an endpoint away to unlink it; moving the whole
|
||||||
|
arrow unlinks both ends. In Arrow mode, hold **Ctrl** while dragging to start a new arrow directly
|
||||||
|
from a number. Deleting a number leaves its arrows in place and removes their links; Undo restores them.
|
||||||
|
**Delete** removes the selected object. **Ctrl+Z** (Undo) reverses additions, moves, resizes and deletions
|
||||||
|
(up to 100 actions). **Ctrl+Y** or **Ctrl+Shift+Z** (Redo) restores undone edits; a new edit clears the
|
||||||
|
redo history. Undo and Redo buttons are disabled when unavailable. Enabled toolbar buttons use a
|
||||||
|
hand cursor. Selection handles are visible only in the editor, never in the copied image.
|
||||||
|
The **palette** changes the selected object's color: arrow, numbered marker, or highlight.
|
||||||
|
It also sets the color for newly drawn objects of every type. With nothing selected, only the
|
||||||
|
color for new objects changes. Number text switches between dark and white for readability.
|
||||||
|
**Highlight (H)** paints a translucent freehand marker stroke. Dark text remains readable beneath
|
||||||
|
the highlight. Drag a selected stroke
|
||||||
|
to move it, scroll over it to change width (4–100 pixels), or use Delete / Ctrl+D / Undo / Redo.
|
||||||
|
Hold Ctrl while drawing to paint a new stroke over an existing object. Highlights render beneath
|
||||||
|
arrows and numbered markers. The palette's color indicator reflects the selected object, or the
|
||||||
|
color for new objects when nothing is selected.
|
||||||
|
The editor status bar shows contextual tool, selection, rotation and text-editing hints.
|
||||||
|
Hover a toolbar button to see its action and shortcut; the title stays **KSwitcher — Screenshot**.
|
||||||
|
**Text (T)** adds text directly on the image. Enter inserts a line break; Ctrl+Enter or a click
|
||||||
|
outside commits the edit, and Escape cancels it. Double-click text or press F2 to edit it again.
|
||||||
|
The **Aa** icon opens the standard font dialog for family, size (6–200 pt), bold and italic.
|
||||||
|
The mouse wheel over text adjusts its font size. Text supports color, dragging, Ctrl+D,
|
||||||
|
Delete and Undo/Redo. Arrow keys move the selected object by one source-image pixel;
|
||||||
|
selecting an arrow caption moves only that caption. Repositioning a caption preserves its
|
||||||
|
automatic angle; only dragging the rotation handle switches to a fixed angle.
|
||||||
|
Text has a rotation handle above its selection. Drag to rotate; hold Shift to snap to 15-degree steps.
|
||||||
|
Double-click an arrow, press F2, or use the label icon to add/edit its caption. Captions follow
|
||||||
|
the arrow when moved or resized; drag the caption to adjust its offset. By default, the caption
|
||||||
|
angle follows the line (or middle curve segment) and stays upright. Rotating its handle manually
|
||||||
|
fixes its angle; Shift snaps to 15-degree steps. Undo restores automatic mode. The Aa icon changes
|
||||||
|
the selected caption font, and the mouse wheel over it changes its size. Captions and rotation
|
||||||
|
are included in clipboard export, cloning and Undo/Redo.
|
||||||
|
**Straight line (L)** creates a line without arrowheads. **Rectangle (R)**, **Circle (O)** and
|
||||||
|
**Ellipse (E)** create transparent shapes with colored outlines. Drag the two corner handles to
|
||||||
|
resize; circles keep equal width and height. Move shapes by their outlines, adjust thickness with
|
||||||
|
the mouse wheel or side handle, recolor, clone with Ctrl+D, and undo/redo as with other objects.
|
||||||
|
Hold Ctrl to start drawing over an existing object. Line style applies to outlines; Ends is disabled
|
||||||
|
for shapes.
|
||||||
|
**Freehand line (F)** draws an opaque curve by following the mouse. The curve is one object:
|
||||||
|
move it, change its color or thickness, clone it, or drag its endpoints. Hold Ctrl to start a new
|
||||||
|
stroke over an existing object. The **Line style** selector supports Solid, Dotted, Dashed and
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
**Escape** cancels region selection or inline text edits; it does not close the editor. Copy closes the editor and publishes the image
|
||||||
|
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**.
|
||||||
|
Each capture opens an independent editor; use the shortcut again to take another screenshot while
|
||||||
|
keeping earlier editors and their annotations open.
|
||||||
|
Captures use ordinary clipboard history settings and limits. Protected video and secure desktops cannot be captured.
|
||||||
|
|
||||||
### Updates
|
### Updates
|
||||||
|
|
||||||
Select **Update** in the tray menu to check the stable Releases of
|
Select **Update** in the tray menu to check the stable Releases of
|
||||||
@@ -72,8 +144,9 @@ intact and is reported when Update is selected. Startup network failures remain
|
|||||||
The helper uses Windows' built-in Windows PowerShell 5.1; no additional runtime is installed.
|
The helper uses Windows' built-in Windows PowerShell 5.1; no additional runtime is installed.
|
||||||
|
|
||||||
Settings and the other utility windows use the destination monitor's DPI for their initial frame,
|
Settings and the other utility windows use the destination monitor's DPI for their initial frame,
|
||||||
controls and fonts. They can be resized, stay within a remaining monitor's work area after display
|
controls and fonts. Settings has a fixed form size, without manual resizing or maximization; other
|
||||||
changes, and provide scrollbars when their content does not fit. Tab navigation scrolls the focused
|
utility windows remain resizable. Windows stay within a remaining monitor's work area after display
|
||||||
|
changes and provide scrollbars when their content does not fit. Tab navigation scrolls the focused
|
||||||
field into view; Shift+wheel scrolls horizontally.
|
field into view; Shift+wheel scrolls horizontally.
|
||||||
|
|
||||||
## Implemented
|
## Implemented
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.3.0 — 2026-10-28
|
## v0.4.0 — 2026-10-28
|
||||||
|
|
||||||
- Move Settings and the snippet editor into the on-demand `KSwitcher.Config.exe` process.
|
- Capture a rectangular screen region with Win+Shift+S (configurable), with dimmed selection and independent editor windows.
|
||||||
- Repeated launches activate the existing configurator. Closing its last window releases the process and its memory.
|
- Annotate screenshots with numbered markers, arrows, straight/freehand lines, highlights, rectangles, circles, ellipses and inline text.
|
||||||
- Apply settings immediately through validated IPC with the resident process; reject conflicting saves instead of overwriting newer settings.
|
- Edit color, line style, arrowheads, thickness, font and size. Link arrow endpoints to markers and attach captions.
|
||||||
- Keep shortcut capture in the resident hook, with no extra global hooks in the configurator.
|
- Captions follow line angle and keep their relative offset until manually rotated. Text rotation supports Shift snapping in 15-degree steps.
|
||||||
- Support packaging, validation and rollback of both executables. About now displays the actual application version.
|
- Move objects one pixel with arrow keys; clone with Ctrl+D; undo/redo edits. Numbered markers reuse gaps.
|
||||||
|
- Compact Lucide toolbar, contextual status bar, pencil cursor and confirmation before discarding an uncopied screenshot. Escape no longer closes the editor.
|
||||||
|
- Preserve the separate on-demand configurator and configurable capture shortcut.
|
||||||
|
|
||||||
Verified: 170 native checks, 51 input/layout checks, 28 updater/metadata checks and 21 configurator process checks.
|
Verified: 173 native checks, 373 input/rendering checks, screenshot editor UI integration checks,
|
||||||
A local before/after measurement after opening and closing Settings reduced resident working set from about 21.0 MiB to 14.7 MiB. Results vary; total memory is higher while the separate configurator is open.
|
28 updater checks and package validation using the unchanged v0.3.0 updater.
|
||||||
|
|
||||||
**Upgrade from v0.2.3 or earlier: install this release manually once.** The old updater rejects the additional EXE.
|
**Upgrade from v0.3.0:** use Update in the tray menu. Both executables are supported by that updater.
|
||||||
Exit KSwitcher, extract the entire ZIP into the installation folder, and retain `data/` and any customized dictionaries.
|
**Upgrade from v0.2.3 or earlier:** install manually once because those updaters reject the additional configurator EXE.
|
||||||
Keep `KSwitcher.exe`, `KSwitcher.Config.exe` and `dictionaries/` together. Launch `KSwitcher.exe`.
|
Exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
||||||
Subsequent updates support both executables through the tray Update menu.
|
|
||||||
|
|
||||||
Download **KSwitcher-win-x64.zip**. The archive contains both applications, offline dictionaries,
|
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains both executables,
|
||||||
documentation and license notices. It does not contain personal settings or clipboard history.
|
offline dictionaries, documentation and license notices; no personal settings or clipboard history.
|
||||||
`SHA256SUMS.txt` contains the archive checksum.
|
|
||||||
|
|
||||||
This folder holds the latest compiled version; older versions remain in Git history and Releases.
|
Build and package:
|
||||||
To refresh the tracked package from source:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\build.ps1 -Stage -Test
|
.\build.ps1 -Stage -Test
|
||||||
.\package.ps1 -PublishRelease
|
.\package.ps1 -PublishRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
Commit the ZIP and checksum together with their source changes. Publish both files as assets of
|
Commit the archive, checksum and sources together. Publish both assets on a stable release whose
|
||||||
one stable release tagged with the executable version, so the updater can verify it.
|
tag matches the executable version.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
104c79b64bb30a2161e3ec2f5108a731da0d274e015d53d8835ea7e64f431ee1 KSwitcher-win-x64.zip
|
c2ad230f9946fddcf6faf17378814cba474c104fa155f63cd1a9ac61fc489ee7 KSwitcher-win-x64.zip
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Push-Location $root
|
|||||||
try {
|
try {
|
||||||
$common = @('-std=c++20','-Os','-DNDEBUG','-DUNICODE','-D_UNICODE','-DWIN32_LEAN_AND_MEAN','-DNOMINMAX','-D_WIN32_WINNT=0x0A00','-Wall','-Wextra','-Wno-missing-field-initializers','-Isrc','-Ivendor','-ffunction-sections','-fdata-sections')
|
$common = @('-std=c++20','-Os','-DNDEBUG','-DUNICODE','-D_UNICODE','-DWIN32_LEAN_AND_MEAN','-DNOMINMAX','-D_WIN32_WINNT=0x0A00','-Wall','-Wextra','-Wno-missing-field-initializers','-Isrc','-Ivendor','-ffunction-sections','-fdata-sections')
|
||||||
$libs = @('-luser32','-lshell32','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
|
$libs = @('-luser32','-lshell32','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
|
||||||
foreach ($name in @('core','platform','autostart','autocorrect','storage','ocr','richtext','update','app','ui','main','configuration','config_main')) {
|
foreach ($name in @('core','platform','autostart','autocorrect','storage','ocr','richtext','update','screenshot','app','ui','main','configuration','config_main')) {
|
||||||
& $compiler @common -c "src/$name.cpp" -o "build/$name.o"
|
& $compiler @common -c "src/$name.cpp" -o "build/$name.o"
|
||||||
if ($LASTEXITCODE) { throw "Compilation failed: $name" }
|
if ($LASTEXITCODE) { throw "Compilation failed: $name" }
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ try {
|
|||||||
& windres --codepage=65001 resources/app.rc -O coff -o build/app-res.o
|
& windres --codepage=65001 resources/app.rc -O coff -o build/app-res.o
|
||||||
if ($LASTEXITCODE) { throw 'Resource compilation failed' }
|
if ($LASTEXITCODE) { throw 'Resource compilation failed' }
|
||||||
New-Item -ItemType Directory -Force build/release | Out-Null
|
New-Item -ItemType Directory -Force build/release | Out-Null
|
||||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/configuration.o build/ui-resident.o build/main.o build/app-res.o @libs -o $executablePath
|
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/app.o build/configuration.o build/ui-resident.o build/main.o build/app-res.o @libs -o $executablePath
|
||||||
if ($LASTEXITCODE) { throw 'Link failed' }
|
if ($LASTEXITCODE) { throw 'Link failed' }
|
||||||
Copy-Item -LiteralPath $executablePath -Destination build/release/KSwitcher.exe -Force
|
Copy-Item -LiteralPath $executablePath -Destination build/release/KSwitcher.exe -Force
|
||||||
$configPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Config.exe'
|
$configPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Config.exe'
|
||||||
@@ -37,15 +37,19 @@ try {
|
|||||||
if ($LASTEXITCODE) { throw 'Test build failed' }
|
if ($LASTEXITCODE) { throw 'Test build failed' }
|
||||||
& .\build\KSwitcherTests.exe
|
& .\build\KSwitcherTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Tests failed' }
|
if ($LASTEXITCODE) { throw 'Tests failed' }
|
||||||
& $compiler @common tests/input.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/configuration.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherInputTests.exe
|
& $compiler @common tests/input.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/app.o build/configuration.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherInputTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Input test build failed' }
|
if ($LASTEXITCODE) { throw 'Input test build failed' }
|
||||||
& .\build\KSwitcherInputTests.exe
|
& .\build\KSwitcherInputTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Input tests failed' }
|
if ($LASTEXITCODE) { throw 'Input tests failed' }
|
||||||
|
& $compiler @common tests/screenshot_ui.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/configuration.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherScreenshotUITests.exe
|
||||||
|
if ($LASTEXITCODE) { throw 'Screenshot UI test build failed' }
|
||||||
|
& .\build\KSwitcherScreenshotUITests.exe
|
||||||
|
if ($LASTEXITCODE) { throw 'Screenshot UI tests failed' }
|
||||||
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/update.ps1 -Executable $executablePath
|
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/update.ps1 -Executable $executablePath
|
||||||
if ($LASTEXITCODE) { throw 'Updater tests failed' }
|
if ($LASTEXITCODE) { throw 'Updater tests failed' }
|
||||||
}
|
}
|
||||||
if ($Integration) {
|
if ($Integration) {
|
||||||
& $compiler @common tests/integration.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/configuration.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherIntegration.exe
|
& $compiler @common tests/integration.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/app.o build/configuration.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherIntegration.exe
|
||||||
if ($LASTEXITCODE) { throw 'Integration test build failed' }
|
if ($LASTEXITCODE) { throw 'Integration test build failed' }
|
||||||
& .\build\KSwitcherIntegration.exe
|
& .\build\KSwitcherIntegration.exe
|
||||||
if ($LASTEXITCODE) { throw 'Integration tests failed' }
|
if ($LASTEXITCODE) { throw 'Integration tests failed' }
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include "resource.h"
|
#include "resource.h"
|
||||||
IDI_KSWITCHER ICON "kswitcher.ico"
|
IDI_KSWITCHER ICON "kswitcher.ico"
|
||||||
|
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,3,0,0
|
FILEVERSION 0,4,0,0
|
||||||
PRODUCTVERSION 0,3,0,0
|
PRODUCTVERSION 0,4,0,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -16,7 +17,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.3.0"
|
VALUE "FileVersion", "0.4.0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
@@ -24,3 +25,51 @@ BEGIN
|
|||||||
VALUE "Translation", 0x409, 1200
|
VALUE "Translation", 0x409, 1200
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
|
300 ICON "icons/lucide/circle-dot-normal.ico"
|
||||||
|
310 ICON "icons/lucide/circle-dot-active.ico"
|
||||||
|
320 ICON "icons/lucide/circle-dot-disabled.ico"
|
||||||
|
301 ICON "icons/lucide/arrow-up-right-normal.ico"
|
||||||
|
311 ICON "icons/lucide/arrow-up-right-active.ico"
|
||||||
|
321 ICON "icons/lucide/arrow-up-right-disabled.ico"
|
||||||
|
302 ICON "icons/lucide/undo-2-normal.ico"
|
||||||
|
312 ICON "icons/lucide/undo-2-active.ico"
|
||||||
|
322 ICON "icons/lucide/undo-2-disabled.ico"
|
||||||
|
303 ICON "icons/lucide/copy-normal.ico"
|
||||||
|
313 ICON "icons/lucide/copy-active.ico"
|
||||||
|
323 ICON "icons/lucide/copy-disabled.ico"
|
||||||
|
304 ICON "icons/lucide/x-normal.ico"
|
||||||
|
314 ICON "icons/lucide/x-active.ico"
|
||||||
|
324 ICON "icons/lucide/x-disabled.ico"
|
||||||
|
305 ICON "icons/lucide/redo-2-normal.ico"
|
||||||
|
315 ICON "icons/lucide/redo-2-active.ico"
|
||||||
|
325 ICON "icons/lucide/redo-2-disabled.ico"
|
||||||
|
306 ICON "icons/lucide/highlighter-normal.ico"
|
||||||
|
316 ICON "icons/lucide/highlighter-active.ico"
|
||||||
|
326 ICON "icons/lucide/highlighter-disabled.ico"
|
||||||
|
307 ICON "icons/lucide/palette-normal.ico"
|
||||||
|
317 ICON "icons/lucide/palette-active.ico"
|
||||||
|
327 ICON "icons/lucide/palette-disabled.ico"
|
||||||
|
308 ICON "icons/lucide/pencil-normal.ico"
|
||||||
|
318 ICON "icons/lucide/pencil-active.ico"
|
||||||
|
328 ICON "icons/lucide/pencil-disabled.ico"
|
||||||
|
|
||||||
|
400 ICON "icons/lucide/slash-normal.ico"
|
||||||
|
410 ICON "icons/lucide/slash-active.ico"
|
||||||
|
420 ICON "icons/lucide/slash-disabled.ico"
|
||||||
|
401 ICON "icons/lucide/square-normal.ico"
|
||||||
|
411 ICON "icons/lucide/square-active.ico"
|
||||||
|
421 ICON "icons/lucide/square-disabled.ico"
|
||||||
|
402 ICON "icons/lucide/circle-normal.ico"
|
||||||
|
412 ICON "icons/lucide/circle-active.ico"
|
||||||
|
422 ICON "icons/lucide/circle-disabled.ico"
|
||||||
|
403 ICON "icons/lucide/ellipse-normal.ico"
|
||||||
|
413 ICON "icons/lucide/ellipse-active.ico"
|
||||||
|
423 ICON "icons/lucide/ellipse-disabled.ico"
|
||||||
|
|
||||||
|
404 ICON "icons/lucide/type-normal.ico"
|
||||||
|
414 ICON "icons/lucide/type-active.ico"
|
||||||
|
424 ICON "icons/lucide/type-disabled.ico"
|
||||||
|
|
||||||
|
405 ICON "icons/lucide/a-large-small-normal.ico"
|
||||||
|
415 ICON "icons/lucide/a-large-small-active.ico"
|
||||||
|
425 ICON "icons/lucide/a-large-small-disabled.ico"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Lucide Icons and Contributors
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The following Lucide icons are derived from the Feather project:
|
||||||
|
|
||||||
|
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
||||||
|
|
||||||
|
The MIT License (MIT) (for the icons listed above)
|
||||||
|
|
||||||
|
Copyright (c) 2013-present Cole Bemis
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Lucide https://github.com/lucide-icons/lucide
|
||||||
|
Commit: be09619c1c1124ad09c19fc3e87ee3b939409bbd
|
||||||
|
SVG originals retained. ICO variants generated with tools/generate-toolbar-icons.py.
|
||||||
|
|
||||||
|
Ellipse is adapted from the pinned Lucide circle SVG (r replaced with rx/ry).
|
||||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m15 16 2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16" />
|
||||||
|
<path d="M15.697 14h5.606" />
|
||||||
|
<path d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16" />
|
||||||
|
<path d="M3.304 13h6.392" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M7 7h10v10" />
|
||||||
|
<path d="M7 17 17 7" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 260 B |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="1" />
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 244 B |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||||
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 339 B |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<ellipse cx="12" cy="12" rx="10" ry="7" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 253 B |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m9 11-6 6v3h9l3-3" />
|
||||||
|
<path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 232 B |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
|
||||||
|
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
|
||||||
|
<circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
|
||||||
|
<circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
|
||||||
|
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
|
||||||
|
<path d="m15 5 4 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 377 B |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="M12 5v14" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 256 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m15 14 5-5-5-5" />
|
||||||
|
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 308 B |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M22 2 2 22" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 261 B |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 4v16" />
|
||||||
|
<path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" />
|
||||||
|
<path d="M9 20h6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M9 14 4 9l5-5" />
|
||||||
|
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 309 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M18 6 6 18" />
|
||||||
|
<path d="m6 6 12 12" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 260 B |
@@ -1,2 +1,3 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#define IDI_KSWITCHER 101
|
#define IDI_KSWITCHER 101
|
||||||
|
#define IDC_SCREENSHOT_PENCIL 500
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ std::vector<std::pair<int, std::wstring>> shortcuts(const Settings &s)
|
|||||||
{Case, s.casing},
|
{Case, s.casing},
|
||||||
{Translit, s.translit},
|
{Translit, s.translit},
|
||||||
{Undo, s.undo},
|
{Undo, s.undo},
|
||||||
{ShowHistory, s.clipboard}};
|
{ShowHistory, s.clipboard},
|
||||||
|
{Screenshot, s.screenshot}};
|
||||||
for (size_t i = 0; i < s.direct.size(); ++i)
|
for (size_t i = 0; i < s.direct.size(); ++i)
|
||||||
out.emplace_back(Direct + (int)i, s.direct[i]);
|
out.emplace_back(Direct + (int)i, s.direct[i]);
|
||||||
return out;
|
return out;
|
||||||
@@ -586,6 +587,11 @@ void App::dispatch(int command)
|
|||||||
return;
|
return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (command == Screenshot)
|
||||||
|
{
|
||||||
|
if (!busy) showScreenshot(*this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (command == ShowHistory)
|
if (command == ShowHistory)
|
||||||
{
|
{
|
||||||
showHistory(*this);
|
showHistory(*this);
|
||||||
@@ -1096,6 +1102,7 @@ void App::trayMenu()
|
|||||||
{
|
{
|
||||||
HMENU menu = CreatePopupMenu();
|
HMENU menu = CreatePopupMenu();
|
||||||
AppendMenuW(menu, MF_STRING, 10, L"Clipboard history");
|
AppendMenuW(menu, MF_STRING, 10, L"Clipboard history");
|
||||||
|
AppendMenuW(menu, MF_STRING, 24, L"Capture screen region…");
|
||||||
AppendMenuW(menu, MF_STRING, 11, L"Settings…");
|
AppendMenuW(menu, MF_STRING, 11, L"Settings…");
|
||||||
SetMenuDefaultItem(menu, 11, FALSE);
|
SetMenuDefaultItem(menu, 11, FALSE);
|
||||||
AppendMenuW(menu, MF_STRING, 12, L"Snippets…");
|
AppendMenuW(menu, MF_STRING, 12, L"Snippets…");
|
||||||
@@ -1127,6 +1134,9 @@ void App::trayMenu()
|
|||||||
{
|
{
|
||||||
switch (cmd)
|
switch (cmd)
|
||||||
{
|
{
|
||||||
|
case 24:
|
||||||
|
dispatch(Screenshot);
|
||||||
|
break;
|
||||||
case 10:
|
case 10:
|
||||||
showHistory(*this);
|
showHistory(*this);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace ks
|
|||||||
inline constexpr UINT TrayMessage = WM_APP + 1, ErrorMessage = WM_APP + 2, HistoryMessage = WM_APP + 3,
|
inline constexpr UINT TrayMessage = WM_APP + 1, ErrorMessage = WM_APP + 2, HistoryMessage = WM_APP + 3,
|
||||||
SnippetMessage = WM_APP + 4;
|
SnippetMessage = WM_APP + 4;
|
||||||
inline constexpr int Toggle = 1, Case = 2, Translit = 3, Undo = 4, ShowHistory = 5, AutoCorrect = 6,
|
inline constexpr int Toggle = 1, Case = 2, Translit = 3, Undo = 4, ShowHistory = 5, AutoCorrect = 6,
|
||||||
Direct = 100;
|
Screenshot = 7, Direct = 100;
|
||||||
inline constexpr UINT AutoCorrectMessage = WM_APP + 6;
|
inline constexpr UINT AutoCorrectMessage = WM_APP + 6;
|
||||||
inline constexpr UINT CaptureHotkeyMessage = WM_APP + 5;
|
inline constexpr UINT CaptureHotkeyMessage = WM_APP + 5;
|
||||||
struct LastTransform
|
struct LastTransform
|
||||||
@@ -100,6 +100,7 @@ class App
|
|||||||
static LRESULT CALLBACK mouseProc(int, WPARAM, LPARAM);
|
static LRESULT CALLBACK mouseProc(int, WPARAM, LPARAM);
|
||||||
};
|
};
|
||||||
void showHistory(App &);
|
void showHistory(App &);
|
||||||
|
void showScreenshot(App &);
|
||||||
void refreshHistory(App &);
|
void refreshHistory(App &);
|
||||||
void showSettings(App &, bool visible = true);
|
void showSettings(App &, bool visible = true);
|
||||||
void showLogs(App &);
|
void showLogs(App &);
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ struct Settings
|
|||||||
L"Far", L"ConEmu64", L"putty", L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
|
L"Far", L"ConEmu64", L"putty", L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
|
||||||
int maxItems = 500, maxMiB = 200, maxImageMiB = 20;
|
int maxItems = 500, maxMiB = 200, maxImageMiB = 20;
|
||||||
std::wstring toggle = L"Ctrl+Alt+Shift+Space", casing = L"Ctrl+Alt+Shift+C",
|
std::wstring toggle = L"Ctrl+Alt+Shift+Space", casing = L"Ctrl+Alt+Shift+C",
|
||||||
translit = L"Ctrl+Alt+Shift+T", undo = L"Ctrl+Alt+Shift+Z", clipboard = L"Ctrl+Shift+V";
|
translit = L"Ctrl+Alt+Shift+T", undo = L"Ctrl+Alt+Shift+Z", clipboard = L"Ctrl+Shift+V",
|
||||||
|
screenshot = L"Win+Shift+S";
|
||||||
std::vector<std::wstring> cycle, direct, directIds;
|
std::vector<std::wstring> cycle, direct, directIds;
|
||||||
std::vector<std::wstring> excluded{L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
|
std::vector<std::wstring> excluded{L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
|
||||||
std::vector<Snippet> snippets;
|
std::vector<Snippet> snippets;
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
|
|||||||
HANDLE mutex = CreateMutexW(nullptr, TRUE, L"Local\\KSwitcher.Native.Singleton");
|
HANDLE mutex = CreateMutexW(nullptr, TRUE, L"Local\\KSwitcher.Native.Singleton");
|
||||||
bool existing = GetLastError() == ERROR_ALREADY_EXISTS;
|
bool existing = GetLastError() == ERROR_ALREADY_EXISTS;
|
||||||
int cmd = ks::ShowHistory;
|
int cmd = ks::ShowHistory;
|
||||||
if (arg == L"--convert")
|
if (arg == L"--capture")
|
||||||
|
cmd = ks::Screenshot;
|
||||||
|
else if (arg == L"--convert")
|
||||||
cmd = ks::Toggle;
|
cmd = ks::Toggle;
|
||||||
else if (arg == L"--case")
|
else if (arg == L"--case")
|
||||||
cmd = ks::Case;
|
cmd = ks::Case;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "platform.h"
|
||||||
|
namespace ks
|
||||||
|
{
|
||||||
|
enum class LineStyle { Solid, Dotted, Dashed, DashDot };
|
||||||
|
enum class LineEnds { End, Start, Both, None };
|
||||||
|
enum class ScreenshotShape { None, Rectangle, Circle, Ellipse };
|
||||||
|
struct ScreenshotOffset { double x, y; };
|
||||||
|
struct ScreenshotMark {
|
||||||
|
POINT from, to; int number; int radius = 15; int thickness = 3;
|
||||||
|
int fromNumber = 0, toNumber = 0;
|
||||||
|
std::vector<POINT> points;
|
||||||
|
COLORREF color = RGB(225,42,50);
|
||||||
|
bool freehand = false;
|
||||||
|
LineStyle style = LineStyle::Solid;
|
||||||
|
LineEnds ends = LineEnds::End;
|
||||||
|
ScreenshotShape shape = ScreenshotShape::None;
|
||||||
|
std::wstring text, fontName=L"Segoe UI";
|
||||||
|
int fontSize=24, fontWeight=FW_NORMAL;
|
||||||
|
bool fontItalic=false;
|
||||||
|
double rotation=0;
|
||||||
|
std::wstring label;
|
||||||
|
ScreenshotOffset labelOffset{0,-24}; // Along the line and perpendicular to it, in image pixels.
|
||||||
|
bool labelRotationManual=false;
|
||||||
|
bool isText() const { return !text.empty(); }
|
||||||
|
bool isHighlight() const { return !points.empty() && !freehand; }
|
||||||
|
bool isLine() const { return !number && !isText() && !isHighlight() && shape==ScreenshotShape::None; }
|
||||||
|
};
|
||||||
|
class ScreenshotHistory {
|
||||||
|
std::deque<std::vector<ScreenshotMark>> past, future;
|
||||||
|
public:
|
||||||
|
void remember(const std::vector<ScreenshotMark> &);
|
||||||
|
bool undo(std::vector<ScreenshotMark> &);
|
||||||
|
bool redo(std::vector<ScreenshotMark> &);
|
||||||
|
bool canUndo() const { return !past.empty(); }
|
||||||
|
bool canRedo() const { return !future.empty(); }
|
||||||
|
};
|
||||||
|
void resolveScreenshotLinks(std::vector<ScreenshotMark> &);
|
||||||
|
void linkScreenshotEndpoint(ScreenshotMark &, bool start, POINT drop,
|
||||||
|
const std::vector<ScreenshotMark> &, double tolerance);
|
||||||
|
enum class MarkPart { None, Body, Start, End, Radius, Thickness, Rotation, Label };
|
||||||
|
POINT screenshotThicknessHandle(const ScreenshotMark &, double clearance = 16);
|
||||||
|
ScreenshotMark cloneScreenshotMark(const ScreenshotMark &, const std::vector<ScreenshotMark> &, int width, int height);
|
||||||
|
int nextScreenshotNumber(const std::vector<ScreenshotMark> &);
|
||||||
|
MarkPart hitScreenshotMark(const ScreenshotMark &, POINT, double tolerance);
|
||||||
|
ScreenshotMark dragScreenshotMark(const ScreenshotMark &, MarkPart, POINT origin, POINT position,
|
||||||
|
int width, int height, bool snapRotation=false);
|
||||||
|
POINT constrainScreenshotShape(ScreenshotShape, POINT anchor, POINT position, int width, int height);
|
||||||
|
RECT screenshotTextBounds(const ScreenshotMark &);
|
||||||
|
POINT screenshotTextRotationHandle(const ScreenshotMark &, double clearance=24);
|
||||||
|
ScreenshotMark screenshotArrowLabel(const ScreenshotMark &);
|
||||||
|
// Render at source resolution; never reads or writes the system clipboard.
|
||||||
|
Clip screenshotClip(HDC source, int width, int height, const std::vector<ScreenshotMark> &marks);
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ bool validId(const std::wstring &id)
|
|||||||
void validateSettings(const Settings &s)
|
void validateSettings(const Settings &s)
|
||||||
{
|
{
|
||||||
std::vector<Hotkey> used;
|
std::vector<Hotkey> used;
|
||||||
std::vector<std::wstring> keys{s.toggle, s.casing, s.translit, s.undo, s.clipboard};
|
std::vector<std::wstring> keys{s.toggle, s.casing, s.translit, s.undo, s.clipboard, s.screenshot};
|
||||||
keys.insert(keys.end(), s.direct.begin(), s.direct.end());
|
keys.insert(keys.end(), s.direct.begin(), s.direct.end());
|
||||||
for (auto &text : keys)
|
for (auto &text : keys)
|
||||||
{
|
{
|
||||||
@@ -150,6 +150,7 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
|||||||
s.translit = str(j, "translitHotkey", s.translit);
|
s.translit = str(j, "translitHotkey", s.translit);
|
||||||
s.undo = str(j, "undoHotkey", s.undo);
|
s.undo = str(j, "undoHotkey", s.undo);
|
||||||
s.clipboard = str(j, "clipboardHotkey", s.clipboard);
|
s.clipboard = str(j, "clipboardHotkey", s.clipboard);
|
||||||
|
s.screenshot = str(j, "screenshotHotkey", s.screenshot);
|
||||||
if (j.contains("layouts"))
|
if (j.contains("layouts"))
|
||||||
{
|
{
|
||||||
s.cycle.clear();
|
s.cycle.clear();
|
||||||
@@ -203,7 +204,8 @@ Bytes encodeSettings(const Settings &s)
|
|||||||
{"caseHotkey", utf8(s.casing)},
|
{"caseHotkey", utf8(s.casing)},
|
||||||
{"translitHotkey", utf8(s.translit)},
|
{"translitHotkey", utf8(s.translit)},
|
||||||
{"undoHotkey", utf8(s.undo)},
|
{"undoHotkey", utf8(s.undo)},
|
||||||
{"clipboardHotkey", utf8(s.clipboard)}};
|
{"clipboardHotkey", utf8(s.clipboard)},
|
||||||
|
{"screenshotHotkey", utf8(s.screenshot)}};
|
||||||
j["layouts"] = Json::array();
|
j["layouts"] = Json::array();
|
||||||
for (auto &id : s.cycle)
|
for (auto &id : s.cycle)
|
||||||
j["layouts"].push_back(utf8(id));
|
j["layouts"].push_back(utf8(id));
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ void error(HWND w, const std::exception &e)
|
|||||||
MessageBoxW(w, wide(e.what()).c_str(), L"KSwitcher", MB_OK | MB_ICONWARNING);
|
MessageBoxW(w, wide(e.what()).c_str(), L"KSwitcher", MB_OK | MB_ICONWARNING);
|
||||||
}
|
}
|
||||||
HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc, int width, int height,
|
HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc, int width, int height,
|
||||||
DWORD extra = 0)
|
DWORD extra = 0, bool resizable = true)
|
||||||
{
|
{
|
||||||
WNDCLASSW wc{};
|
WNDCLASSW wc{};
|
||||||
wc.hInstance = app.instance;
|
wc.hInstance = app.instance;
|
||||||
@@ -267,7 +267,8 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
|
|||||||
RegisterClassW(&wc);
|
RegisterClassW(&wc);
|
||||||
UINT dpi = GetDpiForSystem();
|
UINT dpi = GetDpiForSystem();
|
||||||
RECT rect{0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
|
RECT rect{0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
|
||||||
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_THICKFRAME | WS_MAXIMIZEBOX | extra;
|
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | extra;
|
||||||
|
if (resizable) style |= WS_THICKFRAME | WS_MAXIMIZEBOX;
|
||||||
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
|
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
|
||||||
POINT cursor{};
|
POINT cursor{};
|
||||||
GetCursorPos(&cursor);
|
GetCursorPos(&cursor);
|
||||||
@@ -822,6 +823,7 @@ void fillSettings(HWND w, SettingsView *v)
|
|||||||
value(w, 212, s.translit);
|
value(w, 212, s.translit);
|
||||||
value(w, 213, s.undo);
|
value(w, 213, s.undo);
|
||||||
value(w, 214, s.clipboard);
|
value(w, 214, s.clipboard);
|
||||||
|
value(w, 215, s.screenshot);
|
||||||
value(w, 240, std::to_wstring(s.maxItems));
|
value(w, 240, std::to_wstring(s.maxItems));
|
||||||
value(w, 241, std::to_wstring(s.maxMiB));
|
value(w, 241, std::to_wstring(s.maxMiB));
|
||||||
value(w, 242, std::to_wstring(s.maxImageMiB));
|
value(w, 242, std::to_wstring(s.maxImageMiB));
|
||||||
@@ -871,6 +873,7 @@ Settings collectSettings(HWND w, SettingsView *v)
|
|||||||
s.translit = value(w, 212);
|
s.translit = value(w, 212);
|
||||||
s.undo = value(w, 213);
|
s.undo = value(w, 213);
|
||||||
s.clipboard = value(w, 214);
|
s.clipboard = value(w, 214);
|
||||||
|
s.screenshot = value(w, 215);
|
||||||
s.maxItems = integer(w, 240);
|
s.maxItems = integer(w, 240);
|
||||||
s.maxMiB = integer(w, 241);
|
s.maxMiB = integer(w, 241);
|
||||||
s.maxImageMiB = integer(w, 242);
|
s.maxImageMiB = integer(w, 242);
|
||||||
@@ -1630,7 +1633,7 @@ void showSettings(App &app, bool visible)
|
|||||||
activateWindow(target);
|
activateWindow(target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 780, 660);
|
HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 780, 660, 0, false);
|
||||||
app.settingsWindow = w;
|
app.settingsWindow = w;
|
||||||
label(w, L"Text conversion", 18, 16);
|
label(w, L"Text conversion", 18, 16);
|
||||||
checkbox(w, L"Convert recently typed text when nothing is selected", 201, 18, 43, 390);
|
checkbox(w, L"Convert recently typed text when nothing is selected", 201, 18, 43, 390);
|
||||||
@@ -1642,13 +1645,13 @@ void showSettings(App &app, bool visible)
|
|||||||
checkbox(w, L"Write log", 208, 240, 184, 160);
|
checkbox(w, L"Write log", 208, 240, 184, 160);
|
||||||
label(w, L"Click a shortcut and press the desired keys", 420, 16, 350);
|
label(w, L"Click a shortcut and press the desired keys", 420, 16, 350);
|
||||||
const wchar_t *names[] = {L"Next layout", L"Change case", L"Transliterate", L"Undo",
|
const wchar_t *names[] = {L"Next layout", L"Change case", L"Transliterate", L"Undo",
|
||||||
L"Clipboard history"};
|
L"Clipboard history", L"Screen capture"};
|
||||||
for (int i = 0; i < 5; ++i)
|
for (int i = 0; i < 6; ++i)
|
||||||
{
|
{
|
||||||
label(w, names[i], 420, 48 + i * 30, 115);
|
label(w, names[i], 420, 48 + i * 30, 115);
|
||||||
hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220);
|
hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220);
|
||||||
}
|
}
|
||||||
checkbox(w, L"Automatically correct layout (experimental)", 207, 420, 195, 350);
|
checkbox(w, L"Automatically correct layout (experimental)", 207, 420, 211, 350);
|
||||||
label(w, L"Layout cycle — top to bottom", 18, 210, 400);
|
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,
|
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220,
|
||||||
18, 232, 470, 190);
|
18, 232, 470, 190);
|
||||||
@@ -1718,7 +1721,8 @@ void showShortcuts(App &app)
|
|||||||
{
|
{
|
||||||
auto &s = app.settings;
|
auto &s = app.settings;
|
||||||
std::wstring text = L"Next layout: " + s.toggle + L"\nChange case: " + s.casing + L"\nTransliterate: " +
|
std::wstring text = L"Next layout: " + s.toggle + L"\nChange case: " + s.casing + L"\nTransliterate: " +
|
||||||
s.translit + L"\nUndo: " + s.undo + L"\nClipboard history: " + s.clipboard + L"\n";
|
s.translit + L"\nUndo: " + s.undo + L"\nClipboard history: " + s.clipboard +
|
||||||
|
L"\nScreen capture: " + s.screenshot + L"\n";
|
||||||
for (size_t i = 0; i < std::min(app.layouts.size(), s.direct.size()); ++i)
|
for (size_t i = 0; i < std::min(app.layouts.size(), s.direct.size()); ++i)
|
||||||
if (!s.direct[i].empty())
|
if (!s.direct[i].empty())
|
||||||
text += L"\n" + app.layouts[i].name + L": " + s.direct[i];
|
text += L"\n" + app.layouts[i].name + L": " + s.direct[i];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.3.0";
|
inline constexpr wchar_t AppVersion[] = L"0.4.0";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
struct UpdateResult
|
struct UpdateResult
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "ui_geometry.h"
|
#include "ui_geometry.h"
|
||||||
|
#include "screenshot.h"
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <cmath>
|
||||||
namespace ks {
|
namespace ks {
|
||||||
struct InputRegressionAccess {
|
struct InputRegressionAccess {
|
||||||
static void setup(App &a) { a.hookBindings = {{Toggle,{MOD_CONTROL,VK_F12}}, {Case,{MOD_CONTROL|MOD_SHIFT,'U'}}}; }
|
static void setup(App &a) { a.hookBindings = {{Toggle,{MOD_CONTROL,VK_F12}}, {Case,{MOD_CONTROL|MOD_SHIFT,'U'}}}; }
|
||||||
@@ -29,6 +31,410 @@ int main(int argc, char **argv) {
|
|||||||
// Message-only window: no hooks, synthetic input, clipboard changes or visible editor.
|
// Message-only window: no hooks, synthetic input, clipboard changes or visible editor.
|
||||||
app.window=CreateWindowExW(0,L"STATIC",L"Input tests",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr);
|
app.window=CreateWindowExW(0,L"STATIC",L"Input tests",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr);
|
||||||
require(app.window!=nullptr,"message-only test window");
|
require(app.window!=nullptr,"message-only test window");
|
||||||
|
for(int variant=0;variant<3;++variant) for(int icon=0;icon<15;++icon)
|
||||||
|
for(int size:{22,44,55}) {
|
||||||
|
auto handle=(HICON)LoadImageW(app.instance,MAKEINTRESOURCEW(icon<9?300+variant*10+icon:400+variant*10+icon-9),IMAGE_ICON,size,size,0);
|
||||||
|
require(handle!=nullptr,"Lucide toolbar icon loads at standard and high DPI");
|
||||||
|
DestroyIcon(handle);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ks::ScreenshotHistory history;
|
||||||
|
std::vector<ks::ScreenshotMark> marks;
|
||||||
|
require(!history.undo(marks) && !history.redo(marks),"empty annotation history is safe");
|
||||||
|
history.remember(marks);
|
||||||
|
marks.push_back({{20,20},{20,20},1,24});
|
||||||
|
history.remember(marks);
|
||||||
|
marks.push_back({{44,20},{100,20},0,15,8,1,0});
|
||||||
|
require(history.undo(marks) && marks.size()==1 && history.canRedo(),"undo preserves state for redo");
|
||||||
|
require(history.undo(marks) && marks.empty(),"multiple undo steps restore empty canvas");
|
||||||
|
require(history.redo(marks) && marks[0].radius==24,"redo restores marker size");
|
||||||
|
require(history.redo(marks) && marks.size()==2 && marks[1].thickness==8 && marks[1].fromNumber==1,
|
||||||
|
"redo restores arrow style and endpoint links");
|
||||||
|
require(!history.canRedo(),"redo becomes unavailable after last replay");
|
||||||
|
history.undo(marks);
|
||||||
|
history.remember(marks); marks[0].radius=40;
|
||||||
|
require(!history.canRedo() && !history.redo(marks),"new edit discards redo branch");
|
||||||
|
require(history.undo(marks) && marks[0].radius==24,"new branch remains undoable");
|
||||||
|
ks::ScreenshotHistory bounded;
|
||||||
|
for(int i=0;i<110;++i) { bounded.remember(marks); marks[0].radius=i; }
|
||||||
|
int count=0; while(bounded.undo(marks)) ++count;
|
||||||
|
require(count==100,"annotation undo history stays bounded");
|
||||||
|
count=0; while(bounded.redo(marks)) ++count;
|
||||||
|
require(count==100 && marks[0].radius==109,"bounded history can replay all retained edits");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
HDC dc=CreateCompatibleDC(nullptr);
|
||||||
|
BITMAPINFO info{};
|
||||||
|
info.bmiHeader={sizeof(BITMAPINFOHEADER),160,-100,1,32,BI_RGB};
|
||||||
|
void *pixels{};
|
||||||
|
auto bitmap=CreateDIBSection(dc,&info,DIB_RGB_COLORS,&pixels,nullptr,0);
|
||||||
|
auto old=SelectObject(dc,bitmap);
|
||||||
|
PatBlt(dc,0,0,160,100,WHITENESS);
|
||||||
|
auto plain=ks::screenshotClip(dc,160,100,{});
|
||||||
|
auto marked=ks::screenshotClip(dc,160,100,{{{30,30},{30,30},1},{{60,70},{130,70},0}});
|
||||||
|
auto &bytes=marked.formats[0].data;
|
||||||
|
BITMAPINFOHEADER header{}; memcpy(&header,bytes.data(),sizeof(header));
|
||||||
|
require(header.biWidth==160&&header.biHeight==-100,"capture preserves source resolution and orientation");
|
||||||
|
require(bytes.size()==sizeof(header)+160*100*4,"capture exports complete 32-bit DIB");
|
||||||
|
auto pixel=[&](int x,int y) { auto offset=sizeof(header)+(y*160+x)*4; return RGB(bytes[offset+2],bytes[offset+1],bytes[offset]); };
|
||||||
|
require(pixel(30,20)==RGB(225,42,50),"number badge renders in exported image");
|
||||||
|
require(pixel(90,70)==RGB(225,42,50),"arrow shaft renders in exported image");
|
||||||
|
require(pixel(5,5)==RGB(255,255,255),"unmarked pixels preserved");
|
||||||
|
ks::ScreenshotMark blueArrow{{20,50},{140,50},0}; blueArrow.color=RGB(0,80,220);
|
||||||
|
ks::ScreenshotMark greenBadge{{30,30},{30,30},1}; greenBadge.color=RGB(0,200,80);
|
||||||
|
auto colored=ks::screenshotClip(dc,160,100,{blueArrow,greenBadge});
|
||||||
|
auto colorAt=[&](int x,int y) {
|
||||||
|
auto &b=colored.formats[0].data; auto at=sizeof(BITMAPINFOHEADER)+(y*160+x)*4;
|
||||||
|
return RGB(b[at+2],b[at+1],b[at]);
|
||||||
|
};
|
||||||
|
require(colorAt(70,50)==blueArrow.color && colorAt(132,50)==blueArrow.color,
|
||||||
|
"arrow shaft and head use object color");
|
||||||
|
require(colorAt(30,20)==greenBadge.color,"badge fill uses object color");
|
||||||
|
ks::ScreenshotHistory recolors;
|
||||||
|
std::vector<ks::ScreenshotMark> coloredMarks{blueArrow,greenBadge};
|
||||||
|
recolors.remember(coloredMarks); coloredMarks[0].color=RGB(200,0,100);
|
||||||
|
recolors.remember(coloredMarks); coloredMarks[1].color=RGB(255,255,0);
|
||||||
|
require(recolors.undo(coloredMarks) && coloredMarks[1].color==greenBadge.color,"undo restores badge color");
|
||||||
|
require(recolors.undo(coloredMarks) && coloredMarks[0].color==blueArrow.color,"undo restores arrow color");
|
||||||
|
require(recolors.redo(coloredMarks) && coloredMarks[0].color==RGB(200,0,100),"redo restores arrow recoloring");
|
||||||
|
require(ks::cloneScreenshotMark(greenBadge,{greenBadge},160,100).color==greenBadge.color,
|
||||||
|
"cloned badge retains customized color");
|
||||||
|
ks::ScreenshotMark thickArrow{{20,50},{140,50},0,15,12};
|
||||||
|
auto grip=ks::screenshotThicknessHandle(thickArrow);
|
||||||
|
require(grip.x==80 && grip.y==72,"thickness grip sits beside arrow midpoint");
|
||||||
|
auto thicker=ks::dragScreenshotMark(thickArrow,ks::MarkPart::Thickness,grip,{grip.x,grip.y+4},160,100);
|
||||||
|
require(thicker.thickness==20 && thicker.from.x==20 && thicker.to.x==140,
|
||||||
|
"dragging thickness grip changes width without changing endpoints");
|
||||||
|
require(ks::dragScreenshotMark(thickArrow,ks::MarkPart::Thickness,grip,{grip.x+20,grip.y},160,100).thickness==12,
|
||||||
|
"movement along arrow does not change thickness");
|
||||||
|
require(ks::dragScreenshotMark(thickArrow,ks::MarkPart::Thickness,grip,{grip.x,0},160,100).thickness==1,
|
||||||
|
"thickness grip respects minimum width");
|
||||||
|
auto cloned=ks::cloneScreenshotMark(thickArrow,{},160,100);
|
||||||
|
require(cloned.thickness==12 && cloned.from.x!=thickArrow.from.x && cloned.to.x-cloned.from.x==120,
|
||||||
|
"cloned arrow is offset and preserves dimensions and style");
|
||||||
|
auto thick=ks::screenshotClip(dc,160,100,{thickArrow});
|
||||||
|
auto shaftEdge=sizeof(BITMAPINFOHEADER)+(54*160+40)*4;
|
||||||
|
require(thick.formats[0].data[shaftEdge+2]==225 && thick.formats[0].data[shaftEdge+1]==42,
|
||||||
|
"export renders edited arrow thickness");
|
||||||
|
thickArrow.thickness=1;
|
||||||
|
auto thin=ks::screenshotClip(dc,160,100,{thickArrow});
|
||||||
|
require(thin.formats[0].data[shaftEdge+1]==255,"thin arrow leaves surrounding pixels untouched");
|
||||||
|
thickArrow.thickness=12;
|
||||||
|
require(ks::hitScreenshotMark(thickArrow,{60,55},1)==ks::MarkPart::Body,
|
||||||
|
"thick arrow can be selected along its visible edge");
|
||||||
|
require(ks::dragScreenshotMark(thickArrow,ks::MarkPart::End,{140,50},{145,70},160,100).thickness==12,
|
||||||
|
"editing arrow geometry preserves thickness");
|
||||||
|
ks::ScreenshotMark badge{{60,50},{60,50},2};
|
||||||
|
auto clonedBadge=ks::cloneScreenshotMark(badge,{badge,{{90,20},{90,20},5}},160,100);
|
||||||
|
require(clonedBadge.number==1 && clonedBadge.radius==15 && clonedBadge.from.x==76,
|
||||||
|
"cloned badge fills first numbering gap and keeps diameter");
|
||||||
|
require(ks::nextScreenshotNumber({})==1,"empty capture starts numbering at one");
|
||||||
|
std::vector<ks::ScreenshotMark> numbered{
|
||||||
|
{{10,10},{10,10},4},{{20,10},{20,10},1},{{30,10},{30,10},3},{{0,0},{100,0},0}};
|
||||||
|
require(ks::nextScreenshotNumber(numbered)==2,"new badge fills interior gap regardless of stacking order");
|
||||||
|
numbered.push_back({{40,10},{40,10},2});
|
||||||
|
require(ks::nextScreenshotNumber(numbered)==5,"full sequence advances past existing numbers");
|
||||||
|
numbered.erase(numbered.begin()+1);
|
||||||
|
require(ks::nextScreenshotNumber(numbered)==1,"deleted first number is reused");
|
||||||
|
require(numbered[0].number==4 && numbered[1].number==3,"number allocation never renumbers existing objects");
|
||||||
|
std::vector<ks::ScreenshotMark> linked{
|
||||||
|
{{30,50},{30,50},1}, {{130,50},{130,50},2}, {{30,50},{130,50},0}};
|
||||||
|
ks::linkScreenshotEndpoint(linked[2],true,{30,50},linked,6);
|
||||||
|
ks::linkScreenshotEndpoint(linked[2],false,{130,50},linked,6);
|
||||||
|
ks::resolveScreenshotLinks(linked);
|
||||||
|
require(linked[2].fromNumber==1 && linked[2].toNumber==2 && linked[2].from.x==45 && linked[2].to.x==115,
|
||||||
|
"arrow endpoints attach to badge rims");
|
||||||
|
auto clonedLinked=ks::cloneScreenshotMark(linked[2],linked,160,100);
|
||||||
|
require(clonedLinked.fromNumber==0 && clonedLinked.toNumber==0,"cloned arrow does not retain original links");
|
||||||
|
auto widthLinked=ks::dragScreenshotMark(linked[2],ks::MarkPart::Thickness,{80,70},{80,74},160,100);
|
||||||
|
require(widthLinked.fromNumber==1 && widthLinked.toNumber==2,"thickness handle preserves endpoint links");
|
||||||
|
linked[0].from.y=20; linked[0].to.y=20;
|
||||||
|
linked[1].radius=25;
|
||||||
|
ks::resolveScreenshotLinks(linked);
|
||||||
|
require(linked[2].from.y>20 && linked[2].from.y<50 && linked[2].to.x<110,
|
||||||
|
"linked arrows follow badge movement and resize");
|
||||||
|
auto detached=ks::dragScreenshotMark(linked[2],ks::MarkPart::Start,linked[2].from,{10,90},160,100);
|
||||||
|
require(detached.fromNumber==0 && detached.toNumber==2,"endpoint drag detaches only that endpoint");
|
||||||
|
ks::linkScreenshotEndpoint(detached,true,{10,90},linked,6);
|
||||||
|
require(detached.fromNumber==0,"dropping away from badges remains detached");
|
||||||
|
auto translated=ks::dragScreenshotMark(linked[2],ks::MarkPart::Body,{80,35},{85,40},160,100);
|
||||||
|
require(translated.fromNumber==0 && translated.toNumber==0,"moving whole arrow detaches both endpoints");
|
||||||
|
auto lastPoint=linked[2].from;
|
||||||
|
linked.erase(linked.begin());
|
||||||
|
ks::resolveScreenshotLinks(linked);
|
||||||
|
require(linked[1].fromNumber==0 && linked[1].toNumber==2 && linked[1].from.x==lastPoint.x,
|
||||||
|
"deleting badge detaches without moving orphaned endpoint");
|
||||||
|
linked.push_back({{10,10},{10,10},1});
|
||||||
|
ks::resolveScreenshotLinks(linked);
|
||||||
|
require(linked[1].fromNumber==0,"reused number cannot reconnect a deleted link");
|
||||||
|
require(ks::hitScreenshotMark(badge,{60,50},20)==ks::MarkPart::Body,"badge center moves even with large hit tolerance");
|
||||||
|
require(ks::hitScreenshotMark(badge,{75,50},6)==ks::MarkPart::Radius,"badge rim exposes resize handle");
|
||||||
|
auto enlarged=ks::dragScreenshotMark(badge,ks::MarkPart::Radius,{75,50},{95,50},160,100);
|
||||||
|
require(enlarged.radius==35 && enlarged.number==2,"badge resizes without changing its number");
|
||||||
|
auto large=ks::screenshotClip(dc,160,100,{enlarged});
|
||||||
|
const auto &largeBytes=large.formats[0].data;
|
||||||
|
auto red=sizeof(BITMAPINFOHEADER)+(50*160+90)*4;
|
||||||
|
require(largeBytes[red+2]==225 && largeBytes[red+1]==42,"export uses edited badge diameter");
|
||||||
|
require(ks::dragScreenshotMark(badge,ks::MarkPart::Radius,{75,50},{60,50},160,100).radius==8,
|
||||||
|
"badge diameter has a usable minimum");
|
||||||
|
ks::ScreenshotMark arrow{{20,20},{100,60},0};
|
||||||
|
require(ks::hitScreenshotMark(arrow,{60,40},5)==ks::MarkPart::Body,"arrow shaft selects whole arrow");
|
||||||
|
require(ks::hitScreenshotMark(arrow,{100,60},5)==ks::MarkPart::End,"arrow tip selects endpoint");
|
||||||
|
require(ks::hitScreenshotMark(arrow,{60,80},5)==ks::MarkPart::None,"empty canvas misses arrow");
|
||||||
|
auto moved=ks::dragScreenshotMark(arrow,ks::MarkPart::Body,{60,40},{150,90},160,100);
|
||||||
|
require(moved.to.x==159 && moved.to.y==99 && moved.to.x-moved.from.x==80 && moved.to.y-moved.from.y==40,
|
||||||
|
"moving arrow preserves shape at image boundary");
|
||||||
|
auto stretched=ks::dragScreenshotMark(arrow,ks::MarkPart::End,{100,60},{140,80},160,100);
|
||||||
|
require(stretched.from.x==20 && stretched.from.y==20 && stretched.to.x==140 && stretched.to.y==80,
|
||||||
|
"endpoint drag leaves opposite endpoint fixed");
|
||||||
|
require(ks::screenshotClip(dc,160,100,{}).formats[0].data==plain.formats[0].data,
|
||||||
|
"annotations do not modify original capture");
|
||||||
|
ks::ScreenshotMark highlight{{20,50},{120,50},0,15,24};
|
||||||
|
highlight.points={{20,50},{120,50}}; highlight.color=RGB(255,255,0);
|
||||||
|
SetPixel(dc,60,50,RGB(0,0,0));
|
||||||
|
auto yellow=ks::screenshotClip(dc,160,100,{highlight});
|
||||||
|
auto readPixel=[](const ks::Clip &clip,int x,int y) {
|
||||||
|
auto &b=clip.formats[0].data; auto offset=sizeof(BITMAPINFOHEADER)+(y*160+x)*4;
|
||||||
|
return RGB(b[offset+2],b[offset+1],b[offset]);
|
||||||
|
};
|
||||||
|
require(readPixel(yellow,40,50)==RGB(255,255,153),"highlight is translucent in exported image");
|
||||||
|
require(readPixel(yellow,60,50)==RGB(0,0,0),"highlight keeps black text legible");
|
||||||
|
require(readPixel(yellow,40,10)==RGB(255,255,255),"highlight leaves pixels outside brush unchanged");
|
||||||
|
highlight.points.push_back({20,50});
|
||||||
|
require(readPixel(ks::screenshotClip(dc,160,100,{highlight}),40,50)==RGB(255,255,153),
|
||||||
|
"overlapping a stroke with itself does not accumulate opacity");
|
||||||
|
ks::ScreenshotHistory colorHistory;
|
||||||
|
std::vector<ks::ScreenshotMark> strokes{highlight};
|
||||||
|
colorHistory.remember(strokes); strokes[0].color=RGB(0,255,255);
|
||||||
|
require(readPixel(ks::screenshotClip(dc,160,100,strokes),40,50)==RGB(153,255,255),
|
||||||
|
"changing highlight color rerenders original stroke");
|
||||||
|
require(colorHistory.undo(strokes) && strokes[0].color==RGB(255,255,0),"undo restores highlight color");
|
||||||
|
require(colorHistory.redo(strokes) && strokes[0].color==RGB(0,255,255),"redo restores highlight recolor");
|
||||||
|
require(ks::hitScreenshotMark(highlight,{80,55},1)==ks::MarkPart::Body,"highlight stroke is selectable");
|
||||||
|
require(ks::hitScreenshotMark(highlight,{80,10},1)==ks::MarkPart::None,"highlight hit test excludes empty area");
|
||||||
|
auto movedHighlight=ks::dragScreenshotMark(highlight,ks::MarkPart::Body,{20,50},{30,60},160,100);
|
||||||
|
require(movedHighlight.points[1].x==130 && movedHighlight.points[1].y==60,"moving highlight translates entire stroke");
|
||||||
|
auto cloneHighlight=ks::cloneScreenshotMark(highlight,{highlight},160,100);
|
||||||
|
require(cloneHighlight.points[0].x!=highlight.points[0].x && cloneHighlight.color==highlight.color &&
|
||||||
|
cloneHighlight.thickness==24 && cloneHighlight.number==0,"cloning highlight preserves brush and color");
|
||||||
|
PatBlt(dc,0,0,160,100,WHITENESS);
|
||||||
|
ks::ScreenshotMark line{{20,50},{140,50},0}; line.ends=ks::LineEnds::None;
|
||||||
|
ks::ScreenshotMark textMark{{5,5},{5,5},0}; textMark.text=L"Hello &\r\nСвіт";
|
||||||
|
textMark.fontSize=14; textMark.color=RGB(0,80,180);
|
||||||
|
auto textBounds=ks::screenshotTextBounds(textMark);
|
||||||
|
require(textBounds.right>30 && textBounds.bottom>30,"multiline Unicode text is measured");
|
||||||
|
auto textImage=ks::screenshotClip(dc,160,100,{textMark});
|
||||||
|
int textInk=0;
|
||||||
|
for(int y=5;y<textBounds.bottom;++y) for(int x=5;x<textBounds.right;++x)
|
||||||
|
if(readPixel(textImage,x,y)==textMark.color) ++textInk;
|
||||||
|
require(textInk>20,"text exports visible colored glyphs");
|
||||||
|
require(ks::hitScreenshotMark(textMark,{6,6},2)==ks::MarkPart::Body,"text is movable inside its bounds");
|
||||||
|
require(ks::hitScreenshotMark(textMark,{150,90},2)==ks::MarkPart::None,"text excludes empty canvas");
|
||||||
|
auto bigText=textMark; bigText.fontSize=28;
|
||||||
|
auto bigBounds=ks::screenshotTextBounds(bigText);
|
||||||
|
require(bigBounds.bottom>textBounds.bottom && bigBounds.right>textBounds.right,"font size scales text bounds");
|
||||||
|
auto serifText=textMark; serifText.fontName=L"Courier New"; serifText.fontWeight=FW_BOLD; serifText.fontItalic=true;
|
||||||
|
require(ks::screenshotClip(dc,160,100,{serifText}).formats[0].data!=textImage.formats[0].data,
|
||||||
|
"font family and style affect exported glyphs");
|
||||||
|
auto movedText=ks::dragScreenshotMark(textMark,ks::MarkPart::Body,{6,6},{16,16},160,100);
|
||||||
|
auto rotatedText=textMark; rotatedText.rotation=90;
|
||||||
|
auto rotatedBounds=ks::screenshotTextBounds(rotatedText);
|
||||||
|
require(std::abs((rotatedBounds.right-rotatedBounds.left)-(textBounds.bottom-textBounds.top))<=1,
|
||||||
|
"rotation transforms text bounds");
|
||||||
|
POINT center{(textBounds.left+textBounds.right)/2,(textBounds.top+textBounds.bottom)/2};
|
||||||
|
require(ks::hitScreenshotMark(rotatedText,center,2)==ks::MarkPart::Body,"rotated text hit test follows text");
|
||||||
|
auto rotationGrip=ks::screenshotTextRotationHandle(textMark);
|
||||||
|
auto rotation=ks::dragScreenshotMark(textMark,ks::MarkPart::Rotation,rotationGrip,{center.x+40,center.y-25},160,100,true);
|
||||||
|
require(std::abs(std::remainder(rotation.rotation,15))<0.001 && rotation.rotation!=0,"Shift snaps rotation to 15 degrees");
|
||||||
|
auto reverseRotation=ks::dragScreenshotMark(textMark,ks::MarkPart::Rotation,rotationGrip,{center.x-40,center.y-25},160,100,false);
|
||||||
|
require(reverseRotation.rotation<0,"text rotates in the opposite direction");
|
||||||
|
require(ks::screenshotClip(dc,160,100,{rotatedText}).formats[0].data!=textImage.formats[0].data,
|
||||||
|
"rotation is included in exported text pixels");
|
||||||
|
require(ks::cloneScreenshotMark(rotatedText,{rotatedText},160,100).rotation==90,"cloning preserves text rotation");
|
||||||
|
ks::ScreenshotMark labeled{{20,70},{140,70},0}; labeled.label=L"Label"; labeled.fontSize=12;
|
||||||
|
auto label=ks::screenshotArrowLabel(labeled);
|
||||||
|
auto labelBounds=ks::screenshotTextBounds(label);
|
||||||
|
POINT labelCenter{(labelBounds.left+labelBounds.right)/2,(labelBounds.top+labelBounds.bottom)/2};
|
||||||
|
auto labelGrip=ks::screenshotTextRotationHandle(label);
|
||||||
|
auto diagonal=labeled; diagonal.to={100,150};
|
||||||
|
for(int degrees:{0,45,90,135,180,-135,-90,-45}) {
|
||||||
|
double angle=degrees*3.141592653589793/180;
|
||||||
|
auto turning=labeled; turning.from={100,100};
|
||||||
|
turning.to={100+LONG(std::lround(80*std::cos(angle))),100+LONG(std::lround(80*std::sin(angle)))};
|
||||||
|
auto box=ks::screenshotTextBounds(ks::screenshotArrowLabel(turning));
|
||||||
|
double x=(box.left+box.right)/2.0-(turning.from.x+turning.to.x)/2.0;
|
||||||
|
double y=(box.top+box.bottom)/2.0-(turning.from.y+turning.to.y)/2.0;
|
||||||
|
require(std::abs(-x*std::sin(angle)+y*std::cos(angle)+24)<1.5,
|
||||||
|
"caption keeps its perpendicular distance and side as line rotates");
|
||||||
|
require(std::abs(x*std::cos(angle)+y*std::sin(angle))<1.5,
|
||||||
|
"caption remains centered along rotated line");
|
||||||
|
}
|
||||||
|
auto beforeDrag=ks::screenshotTextBounds(ks::screenshotArrowLabel(diagonal));
|
||||||
|
auto offsetDiagonal=ks::dragScreenshotMark(diagonal,ks::MarkPart::Label,{0,0},{12,-8},200,200);
|
||||||
|
require(!offsetDiagonal.labelRotationManual,"moving caption preserves automatic rotation");
|
||||||
|
auto movedCaptionSlope=ks::dragScreenshotMark(offsetDiagonal,ks::MarkPart::End,offsetDiagonal.to,{20,150},200,200);
|
||||||
|
require(std::abs(ks::screenshotArrowLabel(movedCaptionSlope).rotation-90)<0.001,
|
||||||
|
"repositioned caption continues following line angle");
|
||||||
|
auto afterDrag=ks::screenshotTextBounds(ks::screenshotArrowLabel(offsetDiagonal));
|
||||||
|
require(std::abs(afterDrag.left-beforeDrag.left-12)<=1 && std::abs(afterDrag.top-beforeDrag.top+8)<=1,
|
||||||
|
"dragging diagonal caption follows pointer in screen coordinates");
|
||||||
|
require(std::abs(ks::screenshotArrowLabel(diagonal).rotation-45)<0.001,"caption defaults to line angle");
|
||||||
|
auto changedSlope=ks::dragScreenshotMark(diagonal,ks::MarkPart::End,diagonal.to,{20,150},200,200);
|
||||||
|
require(std::abs(ks::screenshotArrowLabel(changedSlope).rotation-90)<0.001,"automatic caption follows endpoint movement");
|
||||||
|
auto reversed=diagonal; std::swap(reversed.from,reversed.to);
|
||||||
|
require(std::abs(ks::screenshotArrowLabel(reversed).rotation-45)<0.001,"automatic caption remains upright on reversed line");
|
||||||
|
auto diagonalLabel=ks::screenshotArrowLabel(diagonal);
|
||||||
|
auto diagonalGrip=ks::screenshotTextRotationHandle(diagonalLabel);
|
||||||
|
auto clickOnly=ks::dragScreenshotMark(diagonal,ks::MarkPart::Rotation,diagonalGrip,diagonalGrip,200,200,true);
|
||||||
|
require(!clickOnly.labelRotationManual,"clicking rotation handle without dragging preserves automatic angle");
|
||||||
|
auto manual=ks::dragScreenshotMark(diagonal,ks::MarkPart::Rotation,diagonalGrip,{diagonalGrip.x+12,diagonalGrip.y},200,200,false);
|
||||||
|
require(manual.labelRotationManual && manual.rotation>45 && manual.rotation<90,
|
||||||
|
"manual rotation starts at displayed automatic angle without jumping");
|
||||||
|
auto manualResized=ks::dragScreenshotMark(manual,ks::MarkPart::End,manual.to,{20,150},200,200);
|
||||||
|
require(ks::screenshotArrowLabel(manualResized).rotation==manual.rotation,"manual angle survives line resizing");
|
||||||
|
require(ks::cloneScreenshotMark(manual,{manual},200,200).labelRotationManual,"clone preserves manual rotation mode");
|
||||||
|
ks::ScreenshotHistory autoAngleHistory; std::vector<ks::ScreenshotMark> autoAngles{diagonal};
|
||||||
|
autoAngleHistory.remember(autoAngles); autoAngles[0]=manual;
|
||||||
|
require(autoAngleHistory.undo(autoAngles) && !autoAngles[0].labelRotationManual,"undo restores automatic angle mode");
|
||||||
|
require(autoAngleHistory.redo(autoAngles) && autoAngles[0].labelRotationManual,"redo restores manual angle mode");
|
||||||
|
auto curvedCaption=diagonal; curvedCaption.freehand=true;
|
||||||
|
curvedCaption.points={{20,70},{60,70},{60,110},{100,150}};
|
||||||
|
require(std::abs(ks::screenshotArrowLabel(curvedCaption).rotation-90)<0.001,"curve caption follows its middle segment");
|
||||||
|
auto rotatedCaption=ks::dragScreenshotMark(labeled,ks::MarkPart::Rotation,labelGrip,
|
||||||
|
{labelCenter.x+35,labelCenter.y-20},160,100,true);
|
||||||
|
require(rotatedCaption.rotation!=0 && std::abs(std::remainder(rotatedCaption.rotation,15))<0.001,
|
||||||
|
"arrow caption rotation snaps to 15 degrees");
|
||||||
|
require(rotatedCaption.from.x==labeled.from.x && rotatedCaption.from.y==labeled.from.y &&
|
||||||
|
rotatedCaption.to.x==labeled.to.x && rotatedCaption.to.y==labeled.to.y &&
|
||||||
|
rotatedCaption.labelOffset.x==labeled.labelOffset.x && rotatedCaption.labelOffset.y==labeled.labelOffset.y,
|
||||||
|
"rotating caption preserves arrow geometry and caption position");
|
||||||
|
require(ks::screenshotArrowLabel(rotatedCaption).rotation==rotatedCaption.rotation,
|
||||||
|
"linked caption uses the regular text rotation");
|
||||||
|
require(ks::screenshotClip(dc,160,100,{rotatedCaption}).formats[0].data!=ks::screenshotClip(dc,160,100,{labeled}).formats[0].data,
|
||||||
|
"caption rotation is exported");
|
||||||
|
require(ks::cloneScreenshotMark(rotatedCaption,{rotatedCaption},160,100).rotation==rotatedCaption.rotation,
|
||||||
|
"cloning retains caption angle");
|
||||||
|
ks::ScreenshotHistory captionHistory; std::vector<ks::ScreenshotMark> captionMarks{labeled};
|
||||||
|
captionHistory.remember(captionMarks); captionMarks[0]=rotatedCaption;
|
||||||
|
require(captionHistory.undo(captionMarks) && captionMarks[0].rotation==0,"undo restores caption angle");
|
||||||
|
require(captionHistory.redo(captionMarks) && captionMarks[0].rotation==rotatedCaption.rotation,"redo restores caption angle");
|
||||||
|
require(ks::hitScreenshotMark(labeled,labelCenter,2)==ks::MarkPart::Label,"arrow label is independently draggable");
|
||||||
|
auto repositioned=ks::dragScreenshotMark(labeled,ks::MarkPart::Label,labelCenter,{labelCenter.x+10,labelCenter.y-8},160,100);
|
||||||
|
require(repositioned.labelOffset.x==10 && repositioned.labelOffset.y==-32 && repositioned.from.x==20,
|
||||||
|
"label drag changes only its offset");
|
||||||
|
auto movedArrow=ks::dragScreenshotMark(labeled,ks::MarkPart::Body,{20,70},{25,75},160,100);
|
||||||
|
auto movedLabel=ks::screenshotArrowLabel(movedArrow);
|
||||||
|
require(movedLabel.from.x==label.from.x+5 && movedLabel.from.y==label.from.y+5,"label follows arrow movement");
|
||||||
|
auto resizedArrow=ks::dragScreenshotMark(labeled,ks::MarkPart::End,labeled.to,{120,70},160,100);
|
||||||
|
require(ks::screenshotArrowLabel(resizedArrow).from.x==label.from.x-10,"label follows arrow resizing");
|
||||||
|
auto noLabel=labeled; noLabel.label.clear();
|
||||||
|
require(ks::screenshotClip(dc,160,100,{labeled}).formats[0].data!=ks::screenshotClip(dc,160,100,{noLabel}).formats[0].data,
|
||||||
|
"arrow label is included in clipboard export");
|
||||||
|
require(ks::cloneScreenshotMark(labeled,{labeled},160,100).label==labeled.label,"cloning retains arrow label");
|
||||||
|
ks::ScreenshotHistory annotationHistory; std::vector<ks::ScreenshotMark> annotations{rotatedText,labeled};
|
||||||
|
annotationHistory.remember(annotations); annotations[0]=rotation; annotations[1]=repositioned;
|
||||||
|
require(annotationHistory.undo(annotations) && annotations[0].rotation==90 && annotations[1].labelOffset.x==0,
|
||||||
|
"undo restores rotation and label position");
|
||||||
|
require(annotationHistory.redo(annotations) && annotations[0].rotation==rotation.rotation && annotations[1].labelOffset.x==10,
|
||||||
|
"redo restores rotation and label position");
|
||||||
|
require(movedText.from.x==15 && movedText.from.y==15 && movedText.text==textMark.text,"text movement preserves content");
|
||||||
|
auto oversizedText=textMark; oversizedText.fontSize=100;
|
||||||
|
auto oversizedBounds=ks::screenshotTextBounds(oversizedText);
|
||||||
|
require(oversizedBounds.right-oversizedBounds.left>160 && oversizedBounds.bottom-oversizedBounds.top>100,
|
||||||
|
"large text exceeds both canvas dimensions");
|
||||||
|
for(int direction:{-1,1}) {
|
||||||
|
auto moved=ks::dragScreenshotMark(oversizedText,ks::MarkPart::Body,{0,0},{direction*20,direction*20},160,100);
|
||||||
|
require(moved.from.x==oversizedText.from.x+direction*20 && moved.from.y==oversizedText.from.y+direction*20,
|
||||||
|
"oversized text moves in either direction on both axes");
|
||||||
|
auto limited=ks::dragScreenshotMark(oversizedText,ks::MarkPart::Body,{0,0},{direction*10000,direction*10000},160,100);
|
||||||
|
auto bounds=ks::screenshotTextBounds(limited);
|
||||||
|
require(bounds.right>=16 && bounds.left<=144 && bounds.bottom>=16 && bounds.top<=84,
|
||||||
|
"oversized text remains selectable at canvas edges");
|
||||||
|
}
|
||||||
|
auto clonedText=ks::cloneScreenshotMark(serifText,{serifText},160,100);
|
||||||
|
require(clonedText.text==serifText.text && clonedText.fontName==serifText.fontName &&
|
||||||
|
clonedText.fontWeight==FW_BOLD && clonedText.fontItalic,"text cloning preserves font and content");
|
||||||
|
ks::ScreenshotHistory textHistory; std::vector<ks::ScreenshotMark> texts{textMark};
|
||||||
|
textHistory.remember(texts); texts[0]=serifText; texts[0].text=L"Changed";
|
||||||
|
require(textHistory.undo(texts) && texts[0].text==textMark.text && texts[0].fontName==textMark.fontName,
|
||||||
|
"undo restores text and font");
|
||||||
|
require(textHistory.redo(texts) && texts[0].text==L"Changed" && texts[0].fontItalic,
|
||||||
|
"redo restores text edits");
|
||||||
|
for(auto shape:{ks::ScreenshotShape::Rectangle,ks::ScreenshotShape::Circle,ks::ScreenshotShape::Ellipse}) {
|
||||||
|
ks::ScreenshotMark figure{{30,20},{90,80},0}; figure.shape=shape;
|
||||||
|
if(shape==ks::ScreenshotShape::Ellipse) figure.to.x=130;
|
||||||
|
figure.color=RGB(0,140,80); figure.thickness=4;
|
||||||
|
auto image=ks::screenshotClip(dc,160,100,{figure});
|
||||||
|
int mid=(figure.from.x+figure.to.x)/2;
|
||||||
|
require(readPixel(image,mid,20)==figure.color,"shape contour renders in chosen color");
|
||||||
|
require(readPixel(image,mid,50)==RGB(255,255,255),"shape interior remains transparent");
|
||||||
|
require(ks::hitScreenshotMark(figure,{mid,20},2)==ks::MarkPart::Body,"shape contour is movable");
|
||||||
|
require(ks::hitScreenshotMark(figure,{mid,50},2)==ks::MarkPart::None,"shape interior does not intercept objects");
|
||||||
|
auto resized=ks::dragScreenshotMark(figure,ks::MarkPart::End,figure.to,{100,85},160,100);
|
||||||
|
require(resized.shape==shape && resized.to.x==100,"shape corner can resize object");
|
||||||
|
if(shape==ks::ScreenshotShape::Circle)
|
||||||
|
require(resized.to.x-resized.from.x==resized.to.y-resized.from.y,"circle remains round when resized");
|
||||||
|
auto moved=ks::dragScreenshotMark(figure,ks::MarkPart::Body,{mid,20},{mid+5,25},160,100);
|
||||||
|
require(moved.from.x==35 && moved.to.x==figure.to.x+5,"moving shape preserves dimensions");
|
||||||
|
auto duplicate=ks::cloneScreenshotMark(figure,{figure},160,100);
|
||||||
|
require(duplicate.shape==shape && duplicate.color==figure.color,"cloning preserves shape and color");
|
||||||
|
ks::ScreenshotHistory history; std::vector<ks::ScreenshotMark> figures{figure};
|
||||||
|
history.remember(figures); figures[0]=resized;
|
||||||
|
require(history.undo(figures) && figures[0].to.x==figure.to.x,"shape resize can be undone");
|
||||||
|
require(history.redo(figures) && figures[0].to.x==resized.to.x,"shape resize can be redone");
|
||||||
|
}
|
||||||
|
auto circleCorner=ks::constrainScreenshotShape(ks::ScreenshotShape::Circle,{60,60},{0,10},160,100);
|
||||||
|
require(circleCorner.x==0 && circleCorner.y==0,"circle supports reverse drawing and canvas boundaries");
|
||||||
|
auto solidLine=ks::screenshotClip(dc,160,100,{line});
|
||||||
|
require(readPixel(solidLine,28,47)==RGB(255,255,255) && readPixel(solidLine,132,47)==RGB(255,255,255),
|
||||||
|
"plain line has no arrowheads");
|
||||||
|
for(auto ends:{ks::LineEnds::End,ks::LineEnds::Start,ks::LineEnds::Both}) {
|
||||||
|
line.ends=ends;
|
||||||
|
auto image=ks::screenshotClip(dc,160,100,{line});
|
||||||
|
require((readPixel(image,28,47)==line.color)==(ends!=ks::LineEnds::End),"start arrowhead matches ends setting");
|
||||||
|
require((readPixel(image,132,47)==line.color)==(ends!=ks::LineEnds::Start),"end arrowhead matches ends setting");
|
||||||
|
}
|
||||||
|
line.ends=ks::LineEnds::None;
|
||||||
|
std::vector<ks::Bytes> styleImages;
|
||||||
|
ks::ScreenshotMark shaky{{20,50},{140,50},0};
|
||||||
|
shaky.freehand=true; shaky.ends=ks::LineEnds::Both;
|
||||||
|
shaky.points={{20,50},{19,50},{40,50},{120,50},{141,50},{140,50}};
|
||||||
|
auto shakyImage=ks::screenshotClip(dc,160,100,{shaky});
|
||||||
|
require(readPixel(shakyImage,28,47)==shaky.color && readPixel(shakyImage,12,47)==RGB(255,255,255),
|
||||||
|
"start arrowhead points outward despite initial mouse jitter");
|
||||||
|
require(readPixel(shakyImage,132,47)==shaky.color && readPixel(shakyImage,148,47)==RGB(255,255,255),
|
||||||
|
"end arrowhead points outward despite final mouse jitter");
|
||||||
|
for(auto style:{ks::LineStyle::Solid,ks::LineStyle::Dotted,ks::LineStyle::Dashed,ks::LineStyle::DashDot}) {
|
||||||
|
line.style=style;
|
||||||
|
auto image=ks::screenshotClip(dc,160,100,{line});
|
||||||
|
int ink=0,gaps=0;
|
||||||
|
for(int x=25;x<135;++x) { if(readPixel(image,x,50)==line.color) ++ink; else ++gaps; }
|
||||||
|
require(ink>0 && (style==ks::LineStyle::Solid?gaps==0:gaps>0),"line pattern exports visible ink and expected gaps");
|
||||||
|
for(auto &previous:styleImages) require(previous!=image.formats[0].data,"line styles have distinct output");
|
||||||
|
styleImages.push_back(image.formats[0].data);
|
||||||
|
}
|
||||||
|
ks::ScreenshotMark curve{{20,80},{140,30},0}; curve.freehand=true; curve.ends=ks::LineEnds::None;
|
||||||
|
curve.points={{20,80},{50,20},{90,70},{140,30}}; curve.color=RGB(0,80,220);
|
||||||
|
auto curveImage=ks::screenshotClip(dc,160,100,{curve});
|
||||||
|
require(!curve.isHighlight() && readPixel(curveImage,50,20)==curve.color,"freehand line exports opaque curved path");
|
||||||
|
require(ks::hitScreenshotMark(curve,{50,20},3)==ks::MarkPart::Body,"curve hit test follows actual stroke");
|
||||||
|
auto curveMoved=ks::dragScreenshotMark(curve,ks::MarkPart::Body,{50,20},{55,25},160,100);
|
||||||
|
require(curveMoved.points[1].x==55 && curveMoved.points[1].y==25,"curve movement preserves all intermediate points");
|
||||||
|
auto curveEnd=ks::dragScreenshotMark(curve,ks::MarkPart::End,curve.to,{150,40},160,100);
|
||||||
|
require(curveEnd.points.back().x==150 && curveEnd.points.back().y==40 && curveEnd.points[1].y==20,
|
||||||
|
"curve endpoint editing preserves intermediate shape");
|
||||||
|
curve.style=ks::LineStyle::DashDot; curve.ends=ks::LineEnds::Both;
|
||||||
|
auto curveCopy=ks::cloneScreenshotMark(curve,{curve},160,100);
|
||||||
|
require(curveCopy.freehand && curveCopy.style==curve.style && curveCopy.ends==curve.ends,"curve cloning preserves style and arrowheads");
|
||||||
|
ks::ScreenshotHistory lineHistory;
|
||||||
|
std::vector<ks::ScreenshotMark> lines{curve}; lineHistory.remember(lines);
|
||||||
|
lines[0].style=ks::LineStyle::Dotted; lines[0].ends=ks::LineEnds::None;
|
||||||
|
require(lineHistory.undo(lines) && lines[0].style==ks::LineStyle::DashDot && lines[0].ends==ks::LineEnds::Both,
|
||||||
|
"undo restores line pattern and endpoints");
|
||||||
|
require(lineHistory.redo(lines) && lines[0].style==ks::LineStyle::Dotted && lines[0].ends==ks::LineEnds::None,
|
||||||
|
"redo restores line pattern and endpoints");
|
||||||
|
SelectObject(dc,old); DeleteObject(bitmap); DeleteDC(dc);
|
||||||
|
}
|
||||||
auto event=[](DWORD vk,DWORD scan=0,bool own=false) {
|
auto event=[](DWORD vk,DWORD scan=0,bool own=false) {
|
||||||
KBDLLHOOKSTRUCT k{}; k.vkCode=vk; k.scanCode=scan;
|
KBDLLHOOKSTRUCT k{}; k.vkCode=vk; k.scanCode=scan;
|
||||||
k.flags=LLKHF_INJECTED|LLKHF_LOWER_IL_INJECTED;
|
k.flags=LLKHF_INJECTED|LLKHF_LOWER_IL_INJECTED;
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// Exercise the actual editor controls without capturing the desktop or changing the clipboard.
|
||||||
|
// Including the implementation lets this integration test drive private editor state directly.
|
||||||
|
#include "../src/screenshot.cpp"
|
||||||
|
#include <iostream>
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
auto temp=std::filesystem::temp_directory_path()/(L"KSwitcher-editor-test-"+std::to_wstring(GetCurrentProcessId()));
|
||||||
|
try {
|
||||||
|
INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES}; InitCommonControlsEx(&controls);
|
||||||
|
ks::App app(GetModuleHandleW(nullptr),true,temp);
|
||||||
|
app.window=CreateWindowW(L"STATIC",L"",0,0,0,0,0,HWND_MESSAGE,nullptr,app.instance,nullptr);
|
||||||
|
ks::Capture c(app);
|
||||||
|
c.image=std::make_unique<ks::Surface>(320,240);
|
||||||
|
PatBlt(c.image->dc,0,0,320,240,WHITENESS); c.start={0,0}; c.end={320,240};
|
||||||
|
WNDCLASSW cls{}; cls.hInstance=app.instance; cls.lpfnWndProc=ks::captureProc;
|
||||||
|
cls.lpszClassName=L"KSwitcher.Editor.Integration"; RegisterClassW(&cls);
|
||||||
|
auto window=CreateWindowW(cls.lpszClassName,L"Editor test",WS_POPUP,0,0,320,240,app.window,nullptr,app.instance,&c);
|
||||||
|
auto require=[](bool ok,const char *message){ if(!ok) throw std::runtime_error(message); };
|
||||||
|
require(window!=nullptr,"editor creation");
|
||||||
|
ks::edit(window,c); ShowWindow(window,SW_HIDE);
|
||||||
|
require(GetDlgItem(window,50)!=nullptr && c.statusHeight>0,"status bar is created");
|
||||||
|
require(c.pencilCursor!=nullptr,"pencil drawing cursor loads");
|
||||||
|
for(int size:{32,48,64,96}) {
|
||||||
|
auto cursor=(HCURSOR)LoadImageW(app.instance,MAKEINTRESOURCEW(IDC_SCREENSHOT_PENCIL),IMAGE_CURSOR,size,size,0);
|
||||||
|
ICONINFO info{};
|
||||||
|
require(cursor && GetIconInfo(cursor,&info) && !info.fIcon,"pencil cursor supports standard and high DPI");
|
||||||
|
require(info.xHotspot<UINT(size/3) && info.yHotspot>UINT(size*2/3),"pencil cursor draws from its tip");
|
||||||
|
if(info.hbmMask) DeleteObject(info.hbmMask);
|
||||||
|
if(info.hbmColor) DeleteObject(info.hbmColor);
|
||||||
|
DestroyCursor(cursor);
|
||||||
|
}
|
||||||
|
wchar_t windowTitle[128]{}; GetWindowTextW(window,windowTitle,128);
|
||||||
|
require(std::wstring(windowTitle)==L"KSwitcher — Screenshot","title has no usage hints");
|
||||||
|
RECT client{}; GetClientRect(window,&client);
|
||||||
|
require(c.view.bottom<=client.bottom-c.statusHeight,"image does not overlap status bar");
|
||||||
|
ks::updateStatus(c);
|
||||||
|
require(c.statusText.find(L"Freehand")!=std::wstring::npos,"default tool hint is shown");
|
||||||
|
SendMessageW(GetDlgItem(window,13),WM_MOUSEMOVE,0,0);
|
||||||
|
require(c.statusText.find(L"Ctrl+C")!=std::wstring::npos,"button hover shows action shortcut");
|
||||||
|
SendMessageW(GetDlgItem(window,13),WM_MOUSELEAVE,0,0);
|
||||||
|
require(c.statusText.find(L"Freehand")!=std::wstring::npos,"button leave restores contextual hint");
|
||||||
|
for(int id:ks::ToolIds) require(GetDlgItem(window,id)!=nullptr,"all toolbar buttons created");
|
||||||
|
require(GetDlgItem(window,34)!=nullptr && c.toolIcons[0][14]!=nullptr,"font icon created and loaded");
|
||||||
|
ks::Mark text{{20,20},{20,20},0}; text.fontSize=18;
|
||||||
|
ks::beginInlineText(window,c,text,-1);
|
||||||
|
require(c.inlineText!=nullptr,"inline text control opens");
|
||||||
|
ks::updateStatus(c);
|
||||||
|
require(c.statusText.find(L"Ctrl+Enter")!=std::wstring::npos,"inline editing hint shows commit and cancel keys");
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"First line\r\nДругий рядок");
|
||||||
|
require(c.textDraft.text==L"First line\r\nДругий рядок","inline text changes update draft");
|
||||||
|
SendMessageW(window,ks::FinishTextMessage,1,(LPARAM)c.inlineText);
|
||||||
|
require(!c.inlineText && c.marks.size()==1 && c.marks[0].text==text.text+L"First line\r\nДругий рядок","commit adds multiline text");
|
||||||
|
ks::beginInlineText(window,c,c.marks[0],0);
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Cancelled");
|
||||||
|
SendMessageW(window,ks::FinishTextMessage,2,(LPARAM)c.inlineText);
|
||||||
|
require(c.marks[0].text!=L"Cancelled","Escape cancels changes");
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_F2,0);
|
||||||
|
require(c.inlineText!=nullptr,"F2 edits inline");
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Edited"); ks::finishInlineText(window,c,true);
|
||||||
|
require(c.marks[0].text==L"Edited","existing text edit commits");
|
||||||
|
SendMessageW(window,WM_COMMAND,12,0);
|
||||||
|
require(c.marks[0].text!=L"Edited","undo restores original text");
|
||||||
|
SendMessageW(window,WM_COMMAND,18,0);
|
||||||
|
require(c.marks[0].text==L"Edited","redo restores text edit");
|
||||||
|
ks::beginInlineText(window,c,c.marks[0],0);
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L""); ks::finishInlineText(window,c,true);
|
||||||
|
require(c.marks.empty(),"committing empty text deletes object");
|
||||||
|
c.marks.push_back({{30,150},{260,150},0}); c.selected=0;
|
||||||
|
SendMessageW(window,WM_COMMAND,35,0);
|
||||||
|
require(c.inlineText && c.editingLabel,"label button opens inline editor");
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Arrow caption");
|
||||||
|
ks::finishInlineText(window,c,true);
|
||||||
|
require(c.marks.size()==1 && c.marks[0].label==L"Arrow caption" && c.marks[0].isLine(),"label attaches to arrow");
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_F2,0);
|
||||||
|
require(c.inlineText && c.editingLabel,"F2 edits arrow caption");
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L""); ks::finishInlineText(window,c,true);
|
||||||
|
require(c.marks.size()==1 && c.marks[0].label.empty(),"empty label removes caption without deleting arrow");
|
||||||
|
SendMessageW(window,WM_COMMAND,12,0);
|
||||||
|
require(c.marks[0].label==L"Arrow caption","undo restores arrow caption");
|
||||||
|
auto caption=ks::screenshotArrowLabel(c.marks[0]);
|
||||||
|
auto captionGrip=ks::screenshotTextRotationHandle(caption,double(c.unit*0.6)*c.image->width/(c.view.right-c.view.left));
|
||||||
|
c.selected=0;
|
||||||
|
require(ks::markAt(c,captionGrip).second==ks::MarkPart::Rotation,"caption rotation handle is interactive");
|
||||||
|
c.marks[0]=ks::dragScreenshotMark(c.marks[0],ks::MarkPart::Rotation,captionGrip,{captionGrip.x+40,captionGrip.y+20},320,240,true);
|
||||||
|
auto angle=c.marks[0].rotation;
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_F2,0);
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Rotated caption"); ks::finishInlineText(window,c,true);
|
||||||
|
require(c.marks[0].rotation==angle && c.marks[0].label==L"Rotated caption","editing caption preserves its rotation");
|
||||||
|
auto rotating=text; rotating.text=L"Rotate"; c.marks.push_back(rotating); c.selected=1;
|
||||||
|
auto handle=ks::screenshotTextRotationHandle(rotating,double(c.unit*0.6)*c.image->width/(c.view.right-c.view.left));
|
||||||
|
require(ks::markAt(c,handle).second==ks::MarkPart::Rotation,"rotation handle is interactive");
|
||||||
|
c.hoveredPart=ks::MarkPart::Rotation; ks::updateStatus(c);
|
||||||
|
require(c.statusText.find(L"15-degree")!=std::wstring::npos,"rotation hint explains Shift snapping");
|
||||||
|
c.hoveredPart=ks::MarkPart::None;
|
||||||
|
std::vector<ks::Mark> nudgeMarks;
|
||||||
|
nudgeMarks.push_back({{60,60},{60,60},1});
|
||||||
|
nudgeMarks.push_back({{60,60},{120,100},0});
|
||||||
|
auto nudgeText=text; nudgeText.text=L"Move"; nudgeText.from=nudgeText.to={60,60}; nudgeMarks.push_back(nudgeText);
|
||||||
|
for(auto shape:{ks::ScreenshotShape::Rectangle,ks::ScreenshotShape::Circle,ks::ScreenshotShape::Ellipse}) {
|
||||||
|
ks::Mark mark{{60,60},{100,100},0}; mark.shape=shape; nudgeMarks.push_back(mark);
|
||||||
|
}
|
||||||
|
for(bool freehand:{false,true}) {
|
||||||
|
ks::Mark mark{{60,60},{120,100},0}; mark.points={{60,60},{90,80},{120,100}}; mark.freehand=freehand;
|
||||||
|
nudgeMarks.push_back(mark);
|
||||||
|
}
|
||||||
|
for(auto mark:nudgeMarks) {
|
||||||
|
c.marks={mark}; c.selected=0; c.selectedLabel=false;
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_RIGHT,0); SendMessageW(window,WM_KEYDOWN,VK_DOWN,0);
|
||||||
|
require(c.marks[0].from.x==61 && c.marks[0].from.y==61,"every object nudges one pixel per key");
|
||||||
|
if(!mark.points.empty()) require(c.marks[0].points[1].x==91 && c.marks[0].points[1].y==81,"nudge moves entire stroke");
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_LEFT,0); SendMessageW(window,WM_KEYDOWN,VK_UP,0);
|
||||||
|
require(c.marks[0].from.x==60 && c.marks[0].from.y==60,"opposite keys restore original position");
|
||||||
|
}
|
||||||
|
ks::Mark nudgeArrow{{60,60},{140,140},0}; nudgeArrow.label=L"Caption"; nudgeArrow.fontSize=12;
|
||||||
|
c.marks={nudgeArrow}; c.selected=0; c.selectedLabel=true;
|
||||||
|
auto beforeNudge=ks::screenshotTextBounds(ks::screenshotArrowLabel(c.marks[0]));
|
||||||
|
for(int i=0;i<5;++i) SendMessageW(window,WM_KEYDOWN,VK_RIGHT,0);
|
||||||
|
auto afterNudge=ks::screenshotTextBounds(ks::screenshotArrowLabel(c.marks[0]));
|
||||||
|
require(afterNudge.left==beforeNudge.left+5 && afterNudge.top==beforeNudge.top,
|
||||||
|
"caption nudges exact screen pixels at a diagonal angle");
|
||||||
|
require(c.marks[0].from.x==60 && !c.marks[0].labelRotationManual,"caption nudge leaves arrow and automatic angle intact");
|
||||||
|
SendMessageW(window,WM_COMMAND,12,0);
|
||||||
|
require(ks::screenshotTextBounds(ks::screenshotArrowLabel(c.marks[0])).left==beforeNudge.left+4,"undo reverses one nudge");
|
||||||
|
SendMessageW(window,WM_COMMAND,18,0);
|
||||||
|
require(ks::screenshotTextBounds(ks::screenshotArrowLabel(c.marks[0])).left==beforeNudge.left+5,"redo restores one nudge");
|
||||||
|
SetWindowPos(window,nullptr,80,60,720,500,SWP_NOZORDER|SWP_SHOWWINDOW);
|
||||||
|
SetPropW(window,L"ConfirmationCentered",nullptr);
|
||||||
|
SetTimer(window,987,10,[](HWND owner,UINT,UINT_PTR id,DWORD) {
|
||||||
|
EnumThreadWindows(GetCurrentThreadId(),[](HWND popup,LPARAM parent)->BOOL {
|
||||||
|
wchar_t className[64]{}; GetClassNameW(popup,className,64);
|
||||||
|
if(GetWindow(popup,GW_OWNER)==(HWND)parent && std::wstring(className)==L"#32770") {
|
||||||
|
RECT ownerRect{},popupRect{};
|
||||||
|
GetWindowRect((HWND)parent,&ownerRect); GetWindowRect(popup,&popupRect);
|
||||||
|
if(std::abs(ownerRect.left+ownerRect.right-popupRect.left-popupRect.right)<=4 &&
|
||||||
|
std::abs(ownerRect.top+ownerRect.bottom-popupRect.top-popupRect.bottom)<=4)
|
||||||
|
SetPropW((HWND)parent,L"ConfirmationCentered",(HANDLE)1);
|
||||||
|
PostMessageW(popup,TDM_CLICK_BUTTON,IDNO,0);
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
},(LPARAM)owner);
|
||||||
|
KillTimer(owner,id);
|
||||||
|
});
|
||||||
|
SendMessageW(window,WM_CLOSE,0,0);
|
||||||
|
require(IsWindow(window),"declining close confirmation keeps the screenshot open");
|
||||||
|
require(GetPropW(window,L"ConfirmationCentered")!=nullptr,"confirmation is centered on editor instead of screen");
|
||||||
|
RemovePropW(window,L"ConfirmationCentered");
|
||||||
|
c.copied=true;
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_ESCAPE,0);
|
||||||
|
require(IsWindow(window),"Escape does not close the screenshot editor");
|
||||||
|
SendMessageW(window,WM_CLOSE,0,0);
|
||||||
|
require(!IsWindow(window),"copied screenshot closes without confirmation");
|
||||||
|
DestroyWindow(app.window); app.window=nullptr;
|
||||||
|
std::filesystem::remove_all(temp);
|
||||||
|
std::cout<<"Passed screenshot editor UI integration checks\n";
|
||||||
|
} catch(const std::exception &e) { std::cerr<<e.what()<<'\n'; return 1; }
|
||||||
|
}
|
||||||
@@ -428,6 +428,16 @@ int main(int argc, char **argv)
|
|||||||
require(!ks::loadSettings(temp / L"defaults.json", {}).elevatedAutostart,
|
require(!ks::loadSettings(temp / L"defaults.json", {}).elevatedAutostart,
|
||||||
"elevated autostart is opt-in for existing and default settings");
|
"elevated autostart is opt-in for existing and default settings");
|
||||||
auto invalid = settings;
|
auto invalid = settings;
|
||||||
|
auto captureSettings = settings;
|
||||||
|
captureSettings.screenshot = L"Ctrl+Shift+F9";
|
||||||
|
require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshot == captureSettings.screenshot,
|
||||||
|
"screen capture shortcut roundtrip");
|
||||||
|
captureSettings.screenshot = captureSettings.clipboard;
|
||||||
|
rejects([&] { ks::validateSettings(captureSettings); }, "screen capture shortcut duplicates rejected");
|
||||||
|
captureSettings.screenshot.clear();
|
||||||
|
ks::validateSettings(captureSettings);
|
||||||
|
require(ks::loadSettings(temp / L"defaults.json", {}).screenshot == L"Win+Shift+S",
|
||||||
|
"screen capture default for existing settings");
|
||||||
invalid.undo = invalid.toggle;
|
invalid.undo = invalid.toggle;
|
||||||
rejects([&] { ks::validateSettings(invalid); }, "duplicate shortcut rejected");
|
rejects([&] { ks::validateSettings(invalid); }, "duplicate shortcut rejected");
|
||||||
invalid = settings;
|
invalid = settings;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Development only: pip install resvg-py==0.5.0 Pillow.
|
||||||
|
Render the pinned Lucide SVG originals into embedded, multi-resolution Windows icons.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
from io import BytesIO
|
||||||
|
from PIL import Image
|
||||||
|
import resvg_py
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[1] / 'resources' / 'icons' / 'lucide'
|
||||||
|
names = ['circle-dot', 'arrow-up-right', 'undo-2', 'copy', 'x', 'redo-2', 'highlighter', 'palette', 'pencil', 'slash', 'square', 'circle', 'ellipse', 'type', 'a-large-small']
|
||||||
|
for name in names:
|
||||||
|
svg = (root / f'{name}.svg').read_text(encoding='utf-8')
|
||||||
|
for variant, color in [('normal', '#334155'), ('active', '#2563eb'), ('disabled', '#94a3b8')]:
|
||||||
|
png = resvg_py.svg_to_bytes(svg_string=svg.replace('currentColor', color), width=256, height=256)
|
||||||
|
im = Image.open(BytesIO(png)).convert('RGBA')
|
||||||
|
im.save(root / f'{name}-{variant}.ico', sizes=[(s, s) for s in (16,20,22,24,28,32,40,44,48,56,64,96,128,256)])
|
||||||
|
|
||||||
|
# Small local visual QA sheet of the actual downloaded paths.
|
||||||
|
preview = Image.new('RGB', (len(names)*60, 72), '#f8fafc')
|
||||||
|
for i, name in enumerate(names):
|
||||||
|
icon = Image.open(root / f'{name}-normal.ico').convert('RGBA').resize((32,32),Image.Resampling.LANCZOS)
|
||||||
|
preview.paste(icon,(14+i*60,20),icon)
|
||||||
|
preview.save(root / 'preview.png')
|
||||||
|
|
||||||
|
# Cursor uses the same Lucide pencil, with a white outline for dark screenshots.
|
||||||
|
paths = '\n'.join(re.findall(r'<path\b[^>]+/>', (root / 'pencil.svg').read_text(encoding='utf-8')))
|
||||||
|
cursor_svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 28 28" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<g stroke="white" stroke-width="4">{paths}</g><g stroke="#334155" stroke-width="2">{paths}</g></svg>'''
|
||||||
|
cursor_png = resvg_py.svg_to_bytes(svg_string=cursor_svg, width=256, height=256)
|
||||||
|
cursor_image = Image.open(BytesIO(cursor_png)).convert('RGBA')
|
||||||
|
buffer = BytesIO()
|
||||||
|
cursor_image.save(buffer, format='ICO', bitmap_format='bmp', sizes=[(s,s) for s in (32,48,64,96)])
|
||||||
|
cursor = bytearray(buffer.getvalue())
|
||||||
|
struct.pack_into('<H', cursor, 2, 2) # CUR instead of ICO
|
||||||
|
for index in range(struct.unpack_from('<H', cursor, 4)[0]):
|
||||||
|
entry = 6 + index * 16
|
||||||
|
size = cursor[entry] or 256
|
||||||
|
# Drawing point is the tip of the pencil, not the center of its image.
|
||||||
|
struct.pack_into('<HH', cursor, entry+4, round(size*4/28), round(size*24/28))
|
||||||
|
(root / 'pencil.cur').write_bytes(cursor)
|
||||||
@@ -10,6 +10,12 @@ try {
|
|||||||
foreach ($process in $config) { if (-not $process.WaitForExit(10000)) { throw 'Close the configurator before updating.' } }
|
foreach ($process in $config) { if (-not $process.WaitForExit(10000)) { throw 'Close the configurator before updating.' } }
|
||||||
Copy-Item -LiteralPath (Join-Path $root 'build\staged\KSwitcher.Config.exe') -Destination (Join-Path $root 'build\KSwitcher.Config.exe') -Force
|
Copy-Item -LiteralPath (Join-Path $root 'build\staged\KSwitcher.Config.exe') -Destination (Join-Path $root 'build\KSwitcher.Config.exe') -Force
|
||||||
Copy-Item -LiteralPath (Join-Path $root 'build\staged\KSwitcher.exe') -Destination $exe -Force
|
Copy-Item -LiteralPath (Join-Path $root 'build\staged\KSwitcher.exe') -Destination $exe -Force
|
||||||
Start-Process -FilePath $exe -WindowStyle Hidden
|
$eventName = 'Local\KSwitcher.Update.Started.' + [guid]::NewGuid().ToString('N')
|
||||||
|
$started = [System.Threading.EventWaitHandle]::new($false, [System.Threading.EventResetMode]::ManualReset, $eventName)
|
||||||
|
try {
|
||||||
|
$newProcess = Start-Process -FilePath $exe -ArgumentList "--update-ready-event=$eventName" -WorkingDirectory (Split-Path $exe) -WindowStyle Hidden -PassThru
|
||||||
|
if (-not $started.WaitOne(10000)) { throw 'KSwitcher did not confirm startup within 10 seconds.' }
|
||||||
|
if ($newProcess.WaitForExit(1000)) { throw "KSwitcher exited after startup (code $($newProcess.ExitCode))." }
|
||||||
|
} finally { $started.Dispose() }
|
||||||
'OK' | Set-Content -LiteralPath (Join-Path $root 'build\update-result.txt')
|
'OK' | Set-Content -LiteralPath (Join-Path $root 'build\update-result.txt')
|
||||||
} catch { $_.Exception.Message | Set-Content -LiteralPath (Join-Path $root 'build\update-result.txt'); exit 1 }
|
} catch { $_.Exception.Message | Set-Content -LiteralPath (Join-Path $root 'build\update-result.txt'); exit 1 }
|
||||||
|
|||||||