Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d28e28c03c | |||
| 6b180798f5 | |||
| 21a09e33be | |||
| 3dbd7317f1 | |||
| 510d0ed211 | |||
| dd9629f8fd |
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.5.0 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.1 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()
|
||||||
@@ -8,8 +8,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
add_library(kscore src/core.cpp src/platform.cpp src/autostart.cpp src/autocorrect.cpp src/storage.cpp src/ocr.cpp src/richtext.cpp src/update.cpp)
|
add_library(kscore src/core.cpp src/platform.cpp src/autostart.cpp src/autocorrect.cpp src/storage.cpp src/ocr.cpp src/richtext.cpp src/update.cpp)
|
||||||
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 shlwapi advapi32 crypt32 imm32 comctl32 gdi32 ole32 oleaut32 taskschd comdlg32 uuid windowscodecs runtimeobject uiautomationcore)
|
||||||
add_executable(KSwitcher WIN32 src/main.cpp src/app.cpp src/screenshot_launcher.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcher WIN32 src/main.cpp src/app.cpp src/screenshot_launcher.cpp src/screenshot_activation.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,15 +18,15 @@ 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/screenshot_launcher.cpp src/app.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcherIntegration tests/integration.cpp src/screenshot_launcher.cpp src/screenshot_activation.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/screenshot_launcher.cpp src/app.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcherInputTests tests/input.cpp src/screenshot_launcher.cpp src/screenshot_activation.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)
|
||||||
target_compile_definitions(KSwitcherInputTests PRIVATE KS_SCREENSHOT_RESOURCES)
|
target_compile_definitions(KSwitcherInputTests PRIVATE KS_SCREENSHOT_RESOURCES)
|
||||||
add_executable(KSwitcherScreenshotUITests tests/screenshot_ui.cpp resources/app.rc)
|
add_executable(KSwitcherScreenshotUITests tests/screenshot_ui.cpp resources/app.rc)
|
||||||
target_link_libraries(KSwitcherScreenshotUITests PRIVATE kscore)
|
target_link_libraries(KSwitcherScreenshotUITests PRIVATE kscore)
|
||||||
target_compile_definitions(KSwitcherScreenshotUITests PRIVATE KS_SCREENSHOT_RESOURCES)
|
target_compile_definitions(KSwitcherScreenshotUITests PRIVATE KS_SCREENSHOT_RESOURCES)
|
||||||
add_executable(KSwitcherScreenshot WIN32 src/screenshot_main.cpp src/screenshot.cpp resources/app.rc)
|
add_executable(KSwitcherScreenshot WIN32 src/screenshot_main.cpp src/screenshot_notifications.cpp src/screenshot_activation.cpp src/screenshot.cpp resources/app.rc)
|
||||||
set_target_properties(KSwitcherScreenshot PROPERTIES OUTPUT_NAME "KSwitcher.Screenshot")
|
set_target_properties(KSwitcherScreenshot PROPERTIES OUTPUT_NAME "KSwitcher.Screenshot")
|
||||||
target_compile_definitions(KSwitcherScreenshot PRIVATE KS_SCREENSHOT_RESOURCES)
|
target_compile_definitions(KSwitcherScreenshot PRIVATE KS_SCREENSHOT_RESOURCES)
|
||||||
target_link_libraries(KSwitcherScreenshot PRIVATE kscore)
|
target_link_libraries(KSwitcherScreenshot PRIVATE kscore)
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ Native C++20 / Win32 rewrite of the Windows functionality of [Typlo](https://git
|
|||||||
No .NET, Avalonia, WebView, Qt, external graphics engine, or separately installed C++ runtime.
|
No .NET, Avalonia, WebView, Qt, external graphics engine, or separately installed C++ runtime.
|
||||||
The original [Typlo](https://github.com/bitcodepro/Typlo) project and its data remain separate.
|
The original [Typlo](https://github.com/bitcodepro/Typlo) project and its data remain separate.
|
||||||
|
|
||||||
**[User guide / Wiki](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki)** — setup, features, shortcuts, dictionaries, screenshots and troubleshooting (Ukrainian).
|
**[User guide / Wiki](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki)** — setup, features, shortcuts, dictionaries, screenshots and troubleshooting in **English** and **[Українська](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki/Home-uk)**. Each wiki page links to its translation.
|
||||||
|
|
||||||
|
[](https://send.monobank.ua/jar/88h2TddKfS)
|
||||||
|
|
||||||
|
[Donate via Monobank](https://send.monobank.ua/jar/88h2TddKfS) · Thank you for supporting KSwitcher.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
@@ -71,8 +75,22 @@ physical key positions and therefore reset recent-word tracking; selection conve
|
|||||||
Press **Win+Shift+S**, choose **Capture screen region…** in the tray menu, or run
|
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
|
`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.
|
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,
|
The selected area is copied immediately, the fullscreen selection is hidden, and a Windows notification
|
||||||
or annotate first: **N** places sequential numbered red markers with a click, and **A** draws
|
is sent. Click the notification to open the editor. Enable **Open editor after screen capture** in settings
|
||||||
|
to open it automatically; this option is off by default. An immediate notification error still opens the editor.
|
||||||
|
Existing saved preferences are preserved. Windows Do Not Disturb can send notifications directly to the
|
||||||
|
notification center without showing a banner.
|
||||||
|
The preference applies to each new capture, including while other editors are open.
|
||||||
|
KSwitcher also sends a notification with a thumbnail. Notification identity and activation are registered
|
||||||
|
at resident startup and refreshed before sending; registration/submission errors are written to the log.
|
||||||
|
Click the notification to reopen the original screenshot, including after the screenshot process has exited.
|
||||||
|
Originals in the local notification cache are DPAPI-encrypted. Thumbnail PNGs live in a separate
|
||||||
|
per-user cache under ProgramData, readable only by that user and SYSTEM, so the Windows notification
|
||||||
|
broker can load them even when the profile directory is inaccessible to it.
|
||||||
|
Notifications expire after 24 hours. Cache cleanup runs on capture/open; each cache is bounded to
|
||||||
|
40 files and 256 MiB. Expired or evicted notifications cannot reopen their screenshot.
|
||||||
|
In the editor, **Ctrl+C** or **Enter** copies the current result without closing the window.
|
||||||
|
To annotate: **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;
|
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
|
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.
|
(the text scales with it), or either endpoint handle of an arrow to change its length and direction.
|
||||||
@@ -142,7 +160,9 @@ Captures use ordinary clipboard history settings and limits. Protected video and
|
|||||||
|
|
||||||
Select **Update** in the tray menu to check the stable Releases of
|
Select **Update** in the tray menu to check the stable Releases of
|
||||||
[this repository](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/releases).
|
[this repository](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/releases).
|
||||||
A startup check also reports an available version, but never installs it automatically.
|
A background check runs shortly after startup and every 6 hours while KSwitcher is running.
|
||||||
|
It reports each available version once per session and never installs automatically.
|
||||||
|
Network failures stay in diagnostics; the next scheduled check retries. You can also check manually at any time.
|
||||||
Confirm the offered version to download and install it. The updater uses the existing
|
Confirm the offered version to download and install it. The updater uses the existing
|
||||||
`KSwitcher-win-x64.zip` and `SHA256SUMS.txt` assets, checks the checksum, package contents, x64
|
`KSwitcher-win-x64.zip` and `SHA256SUMS.txt` assets, checks the checksum, package contents, x64
|
||||||
executable identity and version, then closes and restarts KSwitcher. Downloads and staging happen
|
executable identity and version, then closes and restarts KSwitcher. Downloads and staging happen
|
||||||
@@ -161,6 +181,8 @@ controls and fonts. Settings has a fixed form size, without manual resizing or m
|
|||||||
utility windows remain resizable. Windows stay within a remaining monitor's work area after display
|
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
|
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.
|
||||||
|
Clipboard history also updates its preview row heights when DPI changes, keeping text,
|
||||||
|
thumbnails and source labels aligned with the scaled fonts and spacing.
|
||||||
|
|
||||||
## Implemented
|
## Implemented
|
||||||
|
|
||||||
@@ -193,8 +215,9 @@ field into view; Shift+wheel scrolls horizontally.
|
|||||||
- Single instance and `--history`, `--convert`, `--case`, `--transliterate`, `--settings`,
|
- Single instance and `--history`, `--convert`, `--case`, `--transliterate`, `--settings`,
|
||||||
`--snippets`, `--exit` commands. `--convert` cycles layouts.
|
`--snippets`, `--exit` commands. `--convert` cycles layouts.
|
||||||
|
|
||||||
Default shortcuts match [Typlo](https://github.com/bitcodepro/Typlo): Ctrl+Alt+Shift+Space (cycle), C (case), T (transliteration),
|
Default text shortcuts match [Typlo](https://github.com/bitcodepro/Typlo): Ctrl+Alt+Shift+Space (cycle), C (case), T (transliteration),
|
||||||
Z (undo), 1/2/3 (first layouts), and Ctrl+Shift+V (history).
|
Z (undo), and 1/2/3 (first layouts). Clipboard history defaults to **Win+V** for new settings;
|
||||||
|
existing saved shortcuts are preserved. While KSwitcher runs, this binding opens its history instead of Windows clipboard history.
|
||||||
|
|
||||||
History controls: Enter to paste, Shift+Enter for plain text, Ctrl+1…9 for quick paste,
|
History controls: Enter to paste, Shift+Enter for plain text, Ctrl+1…9 for quick paste,
|
||||||
Ctrl+P to pin/unpin, Delete to delete, Ctrl+O for the full item, Escape to hide.
|
Ctrl+P to pin/unpin, Delete to delete, Ctrl+O for the full item, Escape to hide.
|
||||||
|
|||||||
Binary file not shown.
+19
-21
@@ -1,28 +1,26 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.4.1 — 2026-10-28
|
## v0.6.3 — 2026-10-29
|
||||||
|
|
||||||
- Fix Settings spacing around Screen capture, automatic layout correction and layout controls.
|
- Hide the fullscreen capture selection before submitting a notification when automatic editor opening is disabled. Previously the selection remained visible during submission, which could suppress the banner under Windows fullscreen notification rules.
|
||||||
- Verify update checksums through .NET SHA-256 without requiring the PowerShell Get-FileHash command. This fixes updates from processes launched with a conflicting PowerShell module environment.
|
- Default to capture → notification → click → editor. **Open editor after screen capture** is now off by default; explicitly saved preferences are preserved.
|
||||||
- Capture a rectangular screen region with Win+Shift+S (configurable), with dimmed selection and independent editor windows.
|
- Retain the editor fallback for immediate notification errors.
|
||||||
- Annotate screenshots with numbered markers, arrows, straight/freehand lines, highlights, rectangles, circles, ellipses and inline text.
|
- Check for updates in the background every 6 hours after the startup check. Notify once per version per session, retry after network failures at the next interval, and install only after confirmation.
|
||||||
- Edit color, line style, arrowheads, thickness, font and size. Link arrow endpoints to markers and attach captions.
|
|
||||||
- Captions follow line angle and keep their relative offset until manually rotated. Text rotation supports Shift snapping in 15-degree steps.
|
|
||||||
- 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: 173 native checks, 379 input/rendering checks, screenshot editor UI integration checks,
|
Verified: 176 native checks, 403 input/layout checks at 240 DPI, screenshot editor UI integration,
|
||||||
29 updater checks, including missing Get-FileHash coverage, and package validation.
|
isolated notification registration checks, and 45 updater checks. The new UI regression test failed
|
||||||
|
before the fix and passes afterward: the selection is hidden at notification submission.
|
||||||
|
Banner behavior still needs confirmation on the affected laptop. Windows Do Not Disturb can
|
||||||
|
send notifications directly to the notification center; asynchronous toast failures are not yet monitored.
|
||||||
|
|
||||||
**Upgrade from v0.3.0 or v0.4.0:** use Update in the tray menu. Both executables are supported.
|
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the
|
||||||
If an older updater reports missing `Get-FileHash`, restart KSwitcher normally from Explorer
|
v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||||
(outside a PowerShell 7 session) or install this ZIP manually once. v0.4.1 removes that dependency.
|
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder.
|
||||||
**Upgrade from v0.2.3 or earlier:** install manually once because those updaters reject the additional configurator EXE.
|
Keep personal `data/` and customized dictionaries.
|
||||||
Exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
|
||||||
|
|
||||||
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains both executables,
|
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains the resident,
|
||||||
offline dictionaries, documentation and license notices; no personal settings or clipboard history.
|
configurator and screenshot executables, a release manifest, offline dictionaries, documentation
|
||||||
|
and license notices; no personal settings or clipboard history.
|
||||||
|
|
||||||
Build and package:
|
Build and package:
|
||||||
|
|
||||||
@@ -31,5 +29,5 @@ Build and package:
|
|||||||
.\package.ps1 -PublishRelease
|
.\package.ps1 -PublishRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
Commit the archive, checksum and sources together. Publish both assets on a stable release whose
|
Commit the archive, checksum and sources together. Publish both assets on a stable
|
||||||
tag matches the executable version.
|
`release-vX.Y.Z` tag matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
7b9f07168023d2ad5c71672d0e699144bc15b11175eb05a7149b72b8be4f90b4 KSwitcher-win-x64.zip
|
a43786b06878da0f069dbad0fa29587a736dd2d8904c7bc991f5cf0c0570a16f KSwitcher-win-x64.zip
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ New-Item -ItemType Directory -Force (Split-Path $executablePath) | Out-Null
|
|||||||
Push-Location $root
|
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','-lshlwapi','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
|
||||||
foreach ($name in @('core','platform','autostart','autocorrect','storage','ocr','richtext','update','screenshot','screenshot_main','screenshot_launcher','app','ui','main','configuration','config_main')) {
|
foreach ($name in @('core','platform','autostart','autocorrect','storage','ocr','richtext','update','screenshot','screenshot_main','screenshot_notifications','screenshot_activation','screenshot_launcher','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" }
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ try {
|
|||||||
& windres --codepage=65001 -DKS_SCREENSHOT_RESOURCES resources/app.rc -O coff -o build/screenshot-res.o
|
& windres --codepage=65001 -DKS_SCREENSHOT_RESOURCES resources/app.rc -O coff -o build/screenshot-res.o
|
||||||
if ($LASTEXITCODE) { throw 'Screenshot resource compilation failed' }
|
if ($LASTEXITCODE) { throw 'Screenshot 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/screenshot_launcher.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_launcher.o build/screenshot_activation.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'
|
||||||
@@ -35,7 +35,7 @@ try {
|
|||||||
if ($LASTEXITCODE) { throw 'Configurator link failed' }
|
if ($LASTEXITCODE) { throw 'Configurator link failed' }
|
||||||
Copy-Item -LiteralPath $configPath -Destination build/release/KSwitcher.Config.exe -Force
|
Copy-Item -LiteralPath $configPath -Destination build/release/KSwitcher.Config.exe -Force
|
||||||
$screenshotPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Screenshot.exe'
|
$screenshotPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Screenshot.exe'
|
||||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/core.o build/platform.o build/screenshot.o build/screenshot_main.o build/screenshot-res.o @libs -o $screenshotPath
|
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/core.o build/platform.o build/ocr.o build/screenshot.o build/screenshot_notifications.o build/screenshot_activation.o build/screenshot_main.o build/screenshot-res.o @libs -o $screenshotPath
|
||||||
if ($LASTEXITCODE) { throw 'Screenshot link failed' }
|
if ($LASTEXITCODE) { throw 'Screenshot link failed' }
|
||||||
Copy-Item -LiteralPath $screenshotPath -Destination build/release/KSwitcher.Screenshot.exe -Force
|
Copy-Item -LiteralPath $screenshotPath -Destination build/release/KSwitcher.Screenshot.exe -Force
|
||||||
if ($Test) {
|
if ($Test) {
|
||||||
@@ -43,19 +43,27 @@ 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/screenshot.o build/screenshot_launcher.o build/app.o build/configuration.o build/ui.o build/screenshot-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/screenshot_launcher.o build/screenshot_activation.o build/app.o build/configuration.o build/ui.o build/screenshot-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/screenshot-res.o @libs -static -s -o build/KSwitcherScreenshotUITests.exe
|
& $compiler @common tests/screenshot_ui.cpp build/core.o build/platform.o build/ocr.o build/screenshot-res.o @libs -static -s -o build/KSwitcherScreenshotUITests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Screenshot UI test build failed' }
|
if ($LASTEXITCODE) { throw 'Screenshot UI test build failed' }
|
||||||
& .\build\KSwitcherScreenshotUITests.exe
|
& .\build\KSwitcherScreenshotUITests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Screenshot UI tests failed' }
|
if ($LASTEXITCODE) { throw 'Screenshot UI tests failed' }
|
||||||
|
$registrationTest = Join-Path (Split-Path $executablePath) 'KSwitcherRegistrationTests.exe'
|
||||||
|
try {
|
||||||
|
& $compiler @common tests/screenshot_registration.cpp build/core.o build/platform.o @libs -static '-Wl,--gc-sections' -s -o $registrationTest
|
||||||
|
if ($LASTEXITCODE) { throw 'Screenshot registration test build failed' }
|
||||||
|
& $registrationTest
|
||||||
|
if ($LASTEXITCODE) { throw 'Screenshot registration tests failed' }
|
||||||
|
} finally { Remove-Item -LiteralPath $registrationTest -ErrorAction SilentlyContinue }
|
||||||
|
|
||||||
& "$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/screenshot.o build/screenshot_launcher.o build/app.o build/configuration.o build/ui.o build/screenshot-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/screenshot_launcher.o build/screenshot_activation.o build/app.o build/configuration.o build/ui.o build/screenshot-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' }
|
||||||
|
|||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,5,0,0
|
FILEVERSION 0,6,3,0
|
||||||
PRODUCTVERSION 0,5,0,0
|
PRODUCTVERSION 0,6,3,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.5.0"
|
VALUE "FileVersion", "0.6.3"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="330" viewBox="0 0 960 330">
|
||||||
|
<rect x="1" y="1" width="958" height="328" rx="24" fill="#F5F3FF" stroke="#DDD6FE" stroke-width="2"/>
|
||||||
|
<g font-family="Segoe UI" fill="#241B46">
|
||||||
|
<text x="40" y="58" font-size="20" font-weight="600" fill="#4F46E5">KSwitcher / Support development</text>
|
||||||
|
<text x="40" y="115" font-size="35" font-weight="700">KSwitcher part of your day?</text>
|
||||||
|
<text x="40" y="159" font-size="35" font-weight="700">Buy the developer a coffee.</text>
|
||||||
|
<text x="40" y="202" font-size="20" fill="#655B7C">Your donation helps the tool keep growing.</text>
|
||||||
|
<rect x="40" y="238" width="350" height="54" rx="12" fill="#4F46E5"/>
|
||||||
|
<text x="215" y="272" text-anchor="middle" font-size="20" font-weight="600" fill="white">Support via monobank</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(759 48) rotate(10 66 66)">
|
||||||
|
<rect y="8" width="132" height="132" rx="26" fill="#C4B5FD"/>
|
||||||
|
<rect width="132" height="132" rx="26" fill="#4F46E5"/>
|
||||||
|
<g transform="translate(14 14) scale(3.25)"><path d="M8 7H12V25H8ZM11 15L22 6L25 9L14 18Z" fill="white"/><path d="M11 14L25 24L22 27L10 18Z" fill="#5EEAD4"/></g>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(699 172) rotate(-8 67 60)">
|
||||||
|
<rect y="7" width="142" height="115" rx="22" fill="#C4B5FD"/>
|
||||||
|
<rect width="142" height="115" rx="22" fill="white" stroke="#DDD6FE" stroke-width="2"/>
|
||||||
|
<g stroke="#4F46E5" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M43 41H92V66Q92 83 75 83H60Q43 83 43 66Z"/>
|
||||||
|
<path d="M92 46H99Q115 46 111 58Q108 67 92 66M37 92H100M56 28V18M69 28V18M82 28V18"/>
|
||||||
|
</g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="330" viewBox="0 0 960 330">
|
||||||
|
<rect x="1" y="1" width="958" height="328" rx="24" fill="#F5F3FF" stroke="#DDD6FE" stroke-width="2"/>
|
||||||
|
<g font-family="Segoe UI" fill="#241B46">
|
||||||
|
<text x="40" y="58" font-size="20" font-weight="600" fill="#4F46E5">KSwitcher / Підтримати розробку</text>
|
||||||
|
<text x="40" y="115" font-size="35" font-weight="700">KSwitcher щодня під рукою?</text>
|
||||||
|
<text x="40" y="159" font-size="35" font-weight="700">Пригостіть розробника кавою.</text>
|
||||||
|
<text x="40" y="202" font-size="20" fill="#655B7C">Ваш донат допомагає розвивати інструмент.</text>
|
||||||
|
<rect x="40" y="238" width="350" height="54" rx="12" fill="#4F46E5"/>
|
||||||
|
<text x="215" y="272" text-anchor="middle" font-size="20" font-weight="600" fill="white">Підтримати через monobank</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(759 48) rotate(10 66 66)">
|
||||||
|
<rect y="8" width="132" height="132" rx="26" fill="#C4B5FD"/>
|
||||||
|
<rect width="132" height="132" rx="26" fill="#4F46E5"/>
|
||||||
|
<g transform="translate(14 14) scale(3.25)"><path d="M8 7H12V25H8ZM11 15L22 6L25 9L14 18Z" fill="white"/><path d="M11 14L25 24L22 27L10 18Z" fill="#5EEAD4"/></g>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(699 172) rotate(-8 67 60)">
|
||||||
|
<rect y="7" width="142" height="115" rx="22" fill="#C4B5FD"/>
|
||||||
|
<rect width="142" height="115" rx="22" fill="white" stroke="#DDD6FE" stroke-width="2"/>
|
||||||
|
<g stroke="#4F46E5" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M43 41H92V66Q92 83 75 83H60Q43 83 43 66Z"/>
|
||||||
|
<path d="M92 46H99Q115 46 111 58Q108 67 92 66M37 92H100M56 28V18M69 28V18M82 28V18"/>
|
||||||
|
</g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
+11
-3
@@ -2,6 +2,7 @@
|
|||||||
#include "../resources/resource.h"
|
#include "../resources/resource.h"
|
||||||
#include "ocr.h"
|
#include "ocr.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
#include "screenshot_notifications.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -1253,8 +1254,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if (MessageBoxW(hwnd, prompt.c_str(), L"KSwitcher Update", MB_YESNO | MB_ICONQUESTION) == IDYES)
|
if (MessageBoxW(hwnd, prompt.c_str(), L"KSwitcher Update", MB_YESNO | MB_ICONQUESTION) == IDYES)
|
||||||
app->checkUpdate(true, true, result->version);
|
app->checkUpdate(true, true, result->version);
|
||||||
}
|
}
|
||||||
else
|
else if (app->notifiedUpdate != result->version)
|
||||||
{
|
{
|
||||||
|
app->notifiedUpdate = result->version;
|
||||||
NOTIFYICONDATAW icon{sizeof(icon)};
|
NOTIFYICONDATAW icon{sizeof(icon)};
|
||||||
icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; icon.dwInfoFlags = NIIF_INFO;
|
icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; icon.dwInfoFlags = NIIF_INFO;
|
||||||
wcscpy_s(icon.szInfoTitle, L"KSwitcher update available");
|
wcscpy_s(icon.szInfoTitle, L"KSwitcher update available");
|
||||||
@@ -1273,7 +1275,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
else if (result->interactive)
|
else if (result->interactive)
|
||||||
MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR);
|
MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR);
|
||||||
else
|
else
|
||||||
diagnosticEvent(L"Startup update check failed: " + result->error);
|
diagnosticEvent(L"Background update check failed: " + result->error);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_HOTKEY:
|
case WM_HOTKEY:
|
||||||
@@ -1337,7 +1339,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
case WM_TIMER:
|
case WM_TIMER:
|
||||||
if (wp == 5)
|
if (wp == 5)
|
||||||
{
|
{
|
||||||
KillTimer(hwnd, 5);
|
// After the initial 5-second check, repeat while the resident stays open.
|
||||||
|
// Rearm even when a check is already running or the previous one failed.
|
||||||
|
SetTimer(hwnd, 5, UpdateCheckIntervalMs, nullptr);
|
||||||
app->checkUpdate(false);
|
app->checkUpdate(false);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1452,6 +1456,9 @@ int App::execute()
|
|||||||
confirmUpdatedStartup(updateStartupEvent);
|
confirmUpdatedStartup(updateStartupEvent);
|
||||||
if (startCommand)
|
if (startCommand)
|
||||||
PostMessageW(window, WM_HOTKEY, startCommand, 0);
|
PostMessageW(window, WM_HOTKEY, startCommand, 0);
|
||||||
|
try { registerScreenshotNotifications(); }
|
||||||
|
catch(const std::exception &e) { log(L"Screenshot notification registration: "+wide(e.what())); }
|
||||||
|
auto screenshotActivation=startScreenshotActivationListener();
|
||||||
MSG msg;
|
MSG msg;
|
||||||
while (GetMessageW(&msg, nullptr, 0, 0) > 0)
|
while (GetMessageW(&msg, nullptr, 0, 0) > 0)
|
||||||
{
|
{
|
||||||
@@ -1470,6 +1477,7 @@ int App::execute()
|
|||||||
TranslateMessage(&msg);
|
TranslateMessage(&msg);
|
||||||
DispatchMessageW(&msg);
|
DispatchMessageW(&msg);
|
||||||
}
|
}
|
||||||
|
if(screenshotActivation) CoRevokeClassObject(screenshotActivation);
|
||||||
return (int)msg.wParam;
|
return (int)msg.wParam;
|
||||||
}
|
}
|
||||||
} // namespace ks
|
} // namespace ks
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class App
|
|||||||
std::thread worker;
|
std::thread worker;
|
||||||
std::thread updateWorker;
|
std::thread updateWorker;
|
||||||
bool updateRunning{}, installingUpdate{};
|
bool updateRunning{}, installingUpdate{};
|
||||||
std::wstring availableUpdate;
|
std::wstring availableUpdate, notifiedUpdate;
|
||||||
bool stop{};
|
bool stop{};
|
||||||
UINT taskbarCreated{};
|
UINT taskbarCreated{};
|
||||||
uint64_t pendingAutoVersion{};
|
uint64_t pendingAutoVersion{};
|
||||||
@@ -99,7 +99,7 @@ class App
|
|||||||
static LRESULT CALLBACK keyProc(int, WPARAM, LPARAM);
|
static LRESULT CALLBACK keyProc(int, WPARAM, LPARAM);
|
||||||
static LRESULT CALLBACK mouseProc(int, WPARAM, LPARAM);
|
static LRESULT CALLBACK mouseProc(int, WPARAM, LPARAM);
|
||||||
};
|
};
|
||||||
void showHistory(App &);
|
void showHistory(App &, bool visible = true);
|
||||||
void launchScreenshot(App &);
|
void launchScreenshot(App &);
|
||||||
void refreshHistory(App &);
|
void refreshHistory(App &);
|
||||||
void showSettings(App &, bool visible = true);
|
void showSettings(App &, bool visible = true);
|
||||||
|
|||||||
+2
-1
@@ -48,6 +48,7 @@ struct Settings
|
|||||||
bool elevatedAutostart = false;
|
bool elevatedAutostart = false;
|
||||||
bool autoCorrect = false;
|
bool autoCorrect = false;
|
||||||
bool writeLog = true;
|
bool writeLog = true;
|
||||||
|
bool screenshotAutoOpenEditor = false;
|
||||||
int autoMaxRank = 0;
|
int autoMaxRank = 0;
|
||||||
int autoMinLength = 3;
|
int autoMinLength = 3;
|
||||||
std::wstring autoPrefixChars = L"([{«“‘\"";
|
std::wstring autoPrefixChars = L"([{«“‘\"";
|
||||||
@@ -59,7 +60,7 @@ 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"Win+V",
|
||||||
screenshot = L"Win+Shift+S";
|
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"};
|
||||||
|
|||||||
+41
-2
@@ -1,4 +1,5 @@
|
|||||||
#include "screenshot.h"
|
#include "screenshot.h"
|
||||||
|
#include "ocr.h"
|
||||||
#include "../resources/resource.h"
|
#include "../resources/resource.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -126,6 +127,7 @@ struct Capture
|
|||||||
std::unique_ptr<Surface> image;
|
std::unique_ptr<Surface> image;
|
||||||
std::unique_ptr<Surface> dimmed, frame;
|
std::unique_ptr<Surface> dimmed, frame;
|
||||||
bool editing{}, dragging{}, arrow{}, owned{}, copied{};
|
bool editing{}, dragging{}, arrow{}, owned{}, copied{};
|
||||||
|
bool autoOpenEditor;
|
||||||
bool highlight{};
|
bool highlight{};
|
||||||
bool freehand=true;
|
bool freehand=true;
|
||||||
bool straight{}, textTool{};
|
bool straight{}, textTool{};
|
||||||
@@ -160,7 +162,7 @@ struct Capture
|
|||||||
int hoveredTool=0;
|
int hoveredTool=0;
|
||||||
MarkPart hoveredPart=MarkPart::None;
|
MarkPart hoveredPart=MarkPart::None;
|
||||||
std::wstring statusText;
|
std::wstring statusText;
|
||||||
explicit Capture(ScreenshotHost &a) : app(a) {}
|
explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {}
|
||||||
~Capture() {
|
~Capture() {
|
||||||
if(pencilCursor) DestroyCursor(pencilCursor);
|
if(pencilCursor) DestroyCursor(pencilCursor);
|
||||||
if (font) DeleteObject(font);
|
if (font) DeleteObject(font);
|
||||||
@@ -801,6 +803,27 @@ void edit(HWND w, Capture &c)
|
|||||||
std::min(LONG(MulDiv(700,GetDpiForWindow(w),96)),work.bottom-work.top-40),SWP_FRAMECHANGED|SWP_SHOWWINDOW);
|
std::min(LONG(MulDiv(700,GetDpiForWindow(w),96)),work.bottom-work.top-40),SWP_FRAMECHANGED|SWP_SHOWWINDOW);
|
||||||
layout(w,c); SetForegroundWindow(w); SetFocus(w);
|
layout(w,c); SetForegroundWindow(w); SetFocus(w);
|
||||||
}
|
}
|
||||||
|
void finishSelection(HWND w,Capture &c,DWORD (*publish)(HWND,const Clip &)=writeClipboard)
|
||||||
|
{
|
||||||
|
if(!c.app.notify) { edit(w,c); return; }
|
||||||
|
auto r=region(c.start,c.end);
|
||||||
|
if(r.right-r.left<2 || r.bottom-r.top<2) return;
|
||||||
|
Surface cropped(r.right-r.left,r.bottom-r.top);
|
||||||
|
BitBlt(cropped.dc,0,0,cropped.width,cropped.height,c.image->dc,r.left,r.top,SRCCOPY);
|
||||||
|
auto clip=screenshotClip(cropped.dc,cropped.width,cropped.height,{});
|
||||||
|
publish(c.app.window,clip);
|
||||||
|
c.copied=true;
|
||||||
|
// A successful toast submission does not guarantee that Windows shows a banner.
|
||||||
|
if(c.autoOpenEditor) edit(w,c);
|
||||||
|
// Leave fullscreen capture before submitting the toast: Windows may suppress
|
||||||
|
// banners while a fullscreen application is visible.
|
||||||
|
else ShowWindow(w,SW_HIDE);
|
||||||
|
bool notified=c.app.notify(clip);
|
||||||
|
if(!c.autoOpenEditor) {
|
||||||
|
if(notified) DestroyWindow(w);
|
||||||
|
else edit(w,c);
|
||||||
|
}
|
||||||
|
}
|
||||||
LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||||
{
|
{
|
||||||
auto c = reinterpret_cast<Capture *>(GetWindowLongPtrW(w,GWLP_USERDATA));
|
auto c = reinterpret_cast<Capture *>(GetWindowLongPtrW(w,GWLP_USERDATA));
|
||||||
@@ -1100,7 +1123,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if (msg==WM_LBUTTONUP)
|
if (msg==WM_LBUTTONUP)
|
||||||
{
|
{
|
||||||
c->dragging=false; ReleaseCapture();
|
c->dragging=false; ReleaseCapture();
|
||||||
if (!c->editing) edit(w,*c);
|
if (!c->editing) { finishSelection(w,*c); return 0; }
|
||||||
else if(c->part!=MarkPart::None)
|
else if(c->part!=MarkPart::None)
|
||||||
{
|
{
|
||||||
if(c->part==MarkPart::Start || c->part==MarkPart::End)
|
if(c->part==MarkPart::Start || c->part==MarkPart::End)
|
||||||
@@ -1513,6 +1536,22 @@ Clip screenshotClip(HDC source, int width, int height, const std::vector<Screens
|
|||||||
memcpy(bytes.data()+sizeof(header),output.pixels,bytes.size()-sizeof(header));
|
memcpy(bytes.data()+sizeof(header),output.pixels,bytes.size()-sizeof(header));
|
||||||
return Clip{{{CF_DIB,L"",std::move(bytes)}}};
|
return Clip{{{CF_DIB,L"",std::move(bytes)}}};
|
||||||
}
|
}
|
||||||
|
void showScreenshotEditor(ScreenshotHost &app,const Clip &clip)
|
||||||
|
{
|
||||||
|
auto pixels=decodeImage(clip);
|
||||||
|
auto c=std::make_unique<Capture>(app);
|
||||||
|
c->image=std::make_unique<Surface>(pixels.width,pixels.height);
|
||||||
|
memcpy(c->image->pixels,pixels.bgra.data(),pixels.bgra.size());
|
||||||
|
c->start={0,0}; c->end={(LONG)pixels.width,(LONG)pixels.height}; c->copied=true;
|
||||||
|
WNDCLASSW cls{}; cls.hInstance=app.instance; cls.lpfnWndProc=captureProc; cls.lpszClassName=L"KSwitcher.Capture";
|
||||||
|
cls.style=CS_DBLCLKS; cls.hCursor=LoadCursorW(nullptr,IDC_CROSS);
|
||||||
|
cls.hIcon=LoadIconW(app.instance,MAKEINTRESOURCEW(IDI_KSWITCHER)); RegisterClassW(&cls);
|
||||||
|
auto window=CreateWindowExW(0,cls.lpszClassName,L"KSwitcher Screenshot",WS_POPUP,0,0,1,1,
|
||||||
|
app.window,nullptr,app.instance,c.get());
|
||||||
|
if(!window) throw std::runtime_error("Cannot open screenshot editor");
|
||||||
|
c->owned=true; ++app.windows;
|
||||||
|
auto state=c.release(); edit(window,*state);
|
||||||
|
}
|
||||||
void showScreenshot(ScreenshotHost &app)
|
void showScreenshot(ScreenshotHost &app)
|
||||||
{
|
{
|
||||||
if (activeCapture) { showRequestedWindow(activeCapture); SetForegroundWindow(activeCapture); return; }
|
if (activeCapture) { showRequestedWindow(activeCapture); SetForegroundWindow(activeCapture); return; }
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ struct ScreenshotHost {
|
|||||||
HINSTANCE instance{};
|
HINSTANCE instance{};
|
||||||
HWND window{};
|
HWND window{};
|
||||||
unsigned windows{};
|
unsigned windows{};
|
||||||
|
bool (*notify)(const Clip &){};
|
||||||
|
bool autoOpenEditor = false;
|
||||||
};
|
};
|
||||||
inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72;
|
inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72;
|
||||||
void showScreenshot(ScreenshotHost &);
|
void showScreenshot(ScreenshotHost &);
|
||||||
|
void showScreenshotEditor(ScreenshotHost &,const Clip &);
|
||||||
enum class LineStyle { Solid, Dotted, Dashed, DashDot };
|
enum class LineStyle { Solid, Dotted, Dashed, DashDot };
|
||||||
enum class LineEnds { End, Start, Both, None };
|
enum class LineEnds { End, Start, Both, None };
|
||||||
enum class ScreenshotShape { None, Rectangle, Circle, Ellipse };
|
enum class ScreenshotShape { None, Rectangle, Circle, Ellipse };
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#include "screenshot_notifications.h"
|
||||||
|
#include <atomic>
|
||||||
|
#include <shlobj.h>
|
||||||
|
#include <propkey.h>
|
||||||
|
#include <propvarutil.h>
|
||||||
|
#include <wrl/client.h>
|
||||||
|
namespace ks {
|
||||||
|
namespace {
|
||||||
|
constexpr wchar_t AppId[]=L"HrynCo.KSwitcher.Screenshot";
|
||||||
|
constexpr GUID ActivatorId{0xe3f78047,0xf0ad,0x49b6,{0x90,0x74,0x41,0x19,0x31,0x70,0x61,0x67}};
|
||||||
|
}
|
||||||
|
namespace {
|
||||||
|
using Microsoft::WRL::ComPtr;
|
||||||
|
void check(HRESULT result) {
|
||||||
|
if(FAILED(result)) throw std::runtime_error("Screenshot registration failed: " + std::to_string((unsigned long)result));
|
||||||
|
}
|
||||||
|
void registryString(const std::wstring &path,const wchar_t *name,const std::wstring &value)
|
||||||
|
{
|
||||||
|
HKEY key{};
|
||||||
|
if(RegCreateKeyExW(HKEY_CURRENT_USER,path.c_str(),0,nullptr,0,KEY_SET_VALUE,nullptr,&key,nullptr)!=ERROR_SUCCESS)
|
||||||
|
throw std::runtime_error("Cannot register screenshot notifications");
|
||||||
|
auto result=RegSetValueExW(key,name,0,REG_SZ,(const BYTE *)value.c_str(),DWORD((value.size()+1)*sizeof(wchar_t)));
|
||||||
|
RegCloseKey(key);
|
||||||
|
if(result!=ERROR_SUCCESS) throw std::runtime_error("Cannot register screenshot notification activation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void registerScreenshotNotifications()
|
||||||
|
{
|
||||||
|
auto exe=(std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe").wstring();
|
||||||
|
if(!std::filesystem::exists(exe)) throw std::runtime_error("Screenshot helper is missing");
|
||||||
|
registryString(L"Software\\Classes\\CLSID\\{e3f78047-f0ad-49b6-9074-411931706167}\\LocalServer32",nullptr,L"\""+exe+L"\" --toast-activated");
|
||||||
|
registryString(L"Software\\Classes\\CLSID\\{e3f78047-f0ad-49b6-9074-411931706167}\\LocalServer32",L"ServerExecutable",exe);
|
||||||
|
auto identity=std::wstring(L"Software\\Classes\\AppUserModelId\\")+AppId;
|
||||||
|
registryString(identity,L"DisplayName",L"KSwitcher Screenshot");
|
||||||
|
registryString(identity,L"IconUri",exe);
|
||||||
|
registryString(identity,L"CustomActivator",L"{e3f78047-f0ad-49b6-9074-411931706167}");
|
||||||
|
// A shortcut supplies desktop notification identity and its COM activation callback.
|
||||||
|
ComPtr<IShellLinkW> link; check(CoCreateInstance(CLSID_ShellLink,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&link)));
|
||||||
|
check(link->SetPath(exe.c_str())); check(link->SetIconLocation(exe.c_str(),0));
|
||||||
|
ComPtr<IPropertyStore> properties; check(link.As(&properties));
|
||||||
|
PROPVARIANT id{}; check(InitPropVariantFromString(AppId,&id));
|
||||||
|
auto result=properties->SetValue(PKEY_AppUserModel_ID,id); PropVariantClear(&id); check(result);
|
||||||
|
GUID activator=ActivatorId;
|
||||||
|
PROPVARIANT clsid{}; clsid.vt=VT_CLSID; clsid.puuid=&activator;
|
||||||
|
check(properties->SetValue(PKEY_AppUserModel_ToastActivatorCLSID,clsid));
|
||||||
|
check(properties->Commit());
|
||||||
|
PWSTR programs{}; check(SHGetKnownFolderPath(FOLDERID_Programs,0,nullptr,&programs));
|
||||||
|
auto shortcut=std::filesystem::path(programs)/L"KSwitcher Screenshot.lnk"; CoTaskMemFree(programs);
|
||||||
|
ComPtr<IPersistFile> file; check(link.As(&file)); check(file->Save(shortcut.c_str(),TRUE));
|
||||||
|
// Notify the shell that notification identity/activation registration changed.
|
||||||
|
SHChangeNotify(SHCNE_ASSOCCHANGED,SHCNF_IDLIST,nullptr,nullptr);
|
||||||
|
}
|
||||||
|
bool validScreenshotToken(const std::wstring &id)
|
||||||
|
{
|
||||||
|
if(id.size()!=36) return false;
|
||||||
|
for(size_t i=0;i<id.size();++i) {
|
||||||
|
if(i==8 || i==13 || i==18 || i==23) { if(id[i]!=L'-') return false; }
|
||||||
|
else if(!((id[i]>=L'0' && id[i]<=L'9') || (id[i]>=L'a' && id[i]<=L'f') || (id[i]>=L'A' && id[i]<=L'F'))) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Windows SDK INotificationActivationCallback ABI (notificationactivationcallback.h).
|
||||||
|
constexpr GUID CallbackId{0x53e31837,0x6600,0x4a81,{0x93,0x95,0x75,0xcf,0xfe,0x74,0x6f,0x94}};
|
||||||
|
struct NotificationInput { LPCWSTR key,value; };
|
||||||
|
struct NotificationCallback : IUnknown {
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE Activate(LPCWSTR,LPCWSTR,const NotificationInput *,ULONG)=0;
|
||||||
|
};
|
||||||
|
class Activator final : public NotificationCallback {
|
||||||
|
std::atomic<ULONG> references{1};
|
||||||
|
DWORD thread=GetCurrentThreadId();
|
||||||
|
std::wstring target;
|
||||||
|
bool quit;
|
||||||
|
public:
|
||||||
|
Activator(std::wstring path,bool shouldQuit):target(std::move(path)),quit(shouldQuit) {}
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID id,void **out) override {
|
||||||
|
if(!out) return E_POINTER;
|
||||||
|
*out=nullptr;
|
||||||
|
if(id!=IID_IUnknown && id!=CallbackId) return E_NOINTERFACE;
|
||||||
|
*out=static_cast<NotificationCallback *>(this); AddRef(); return S_OK;
|
||||||
|
}
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override { return ++references; }
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override { auto count=--references; if(!count) delete this; return count; }
|
||||||
|
HRESULT STDMETHODCALLTYPE Activate(LPCWSTR app,LPCWSTR args,const NotificationInput *,ULONG) override {
|
||||||
|
if(!app || std::wstring(app)!=AppId || !args || !validScreenshotToken(args)) return E_INVALIDARG;
|
||||||
|
auto path=target; auto command=L"\""+path+L"\" --open \"kswitcher-screenshot:"+args+L"\"";
|
||||||
|
STARTUPINFOW startup{sizeof(startup)}; PROCESS_INFORMATION process{};
|
||||||
|
auto ok=CreateProcessW(path.c_str(),command.data(),nullptr,nullptr,FALSE,CREATE_SUSPENDED,nullptr,nullptr,&startup,&process);
|
||||||
|
auto result=ok?S_OK:HRESULT_FROM_WIN32(GetLastError());
|
||||||
|
if(ok) {
|
||||||
|
AllowSetForegroundWindow(process.dwProcessId); ResumeThread(process.hThread);
|
||||||
|
CloseHandle(process.hThread); CloseHandle(process.hProcess);
|
||||||
|
}
|
||||||
|
if(quit) PostThreadMessageW(thread,WM_QUIT,0,0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
class ActivatorFactory final : public IClassFactory {
|
||||||
|
std::atomic<ULONG> references{1};
|
||||||
|
std::wstring target;
|
||||||
|
bool quit;
|
||||||
|
public:
|
||||||
|
ActivatorFactory(std::wstring path,bool shouldQuit):target(std::move(path)),quit(shouldQuit) {}
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID id,void **out) override {
|
||||||
|
if(!out) return E_POINTER;
|
||||||
|
*out=nullptr;
|
||||||
|
if(id!=IID_IUnknown && id!=IID_IClassFactory) return E_NOINTERFACE;
|
||||||
|
*out=static_cast<IClassFactory *>(this); AddRef(); return S_OK;
|
||||||
|
}
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override { return ++references; }
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override { auto count=--references; if(!count) delete this; return count; }
|
||||||
|
HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown *outer,REFIID id,void **out) override {
|
||||||
|
if(outer) return CLASS_E_NOAGGREGATION;
|
||||||
|
auto object=new Activator(target,quit); auto result=object->QueryInterface(id,out); object->Release(); return result;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE LockServer(BOOL) override { return S_OK; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
int runScreenshotToastActivator()
|
||||||
|
{
|
||||||
|
if(FAILED(CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED))) return 1;
|
||||||
|
MSG message{}; PeekMessageW(&message,nullptr,0,0,PM_NOREMOVE);
|
||||||
|
auto factory=new ActivatorFactory(executable(),true); DWORD registration{};
|
||||||
|
auto result=CoRegisterClassObject(ActivatorId,factory,CLSCTX_LOCAL_SERVER,REGCLS_MULTIPLEUSE,®istration);
|
||||||
|
factory->Release();
|
||||||
|
if(SUCCEEDED(result)) {
|
||||||
|
auto timer=SetTimer(nullptr,0,60000,[](HWND,UINT,UINT_PTR,DWORD){PostQuitMessage(0);});
|
||||||
|
while(GetMessageW(&message,nullptr,0,0)>0) { TranslateMessage(&message); DispatchMessageW(&message); }
|
||||||
|
if(timer) KillTimer(nullptr,timer);
|
||||||
|
CoRevokeClassObject(registration);
|
||||||
|
}
|
||||||
|
CoUninitialize(); return FAILED(result)?1:0;
|
||||||
|
}
|
||||||
|
DWORD startScreenshotActivationListener()
|
||||||
|
{
|
||||||
|
auto target=std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe";
|
||||||
|
auto factory=new ActivatorFactory(target.wstring(),false);
|
||||||
|
DWORD registration{};
|
||||||
|
auto result=CoRegisterClassObject(ActivatorId,factory,CLSCTX_LOCAL_SERVER,REGCLS_MULTIPLEUSE,®istration);
|
||||||
|
factory->Release();
|
||||||
|
return SUCCEEDED(result)?registration:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
|
||||||
namespace ks {
|
namespace ks {
|
||||||
void launchScreenshot(App &)
|
void launchScreenshot(App &app)
|
||||||
{
|
{
|
||||||
auto path=std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe";
|
auto path=std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe";
|
||||||
if (!std::filesystem::exists(path))
|
if (!std::filesystem::exists(path))
|
||||||
throw std::runtime_error("KSwitcher.Screenshot.exe is missing. Extract the complete application package.");
|
throw std::runtime_error("KSwitcher.Screenshot.exe is missing. Extract the complete application package.");
|
||||||
auto command=L"\""+path.wstring()+L"\"";
|
auto command=L"\""+path.wstring()+L"\" --auto-open-editor="+
|
||||||
|
(app.settings.screenshotAutoOpenEditor?L"1":L"0");
|
||||||
|
if(app.settings.writeLog) command+=L" --write-log";
|
||||||
STARTUPINFOW startup{sizeof(startup)};
|
STARTUPINFOW startup{sizeof(startup)};
|
||||||
PROCESS_INFORMATION process{};
|
PROCESS_INFORMATION process{};
|
||||||
// Grant foreground rights before the child's first selection window opens.
|
// Grant foreground rights before the child's first selection window opens.
|
||||||
|
|||||||
+54
-4
@@ -1,5 +1,8 @@
|
|||||||
#include "screenshot.h"
|
#include "screenshot.h"
|
||||||
|
#include "screenshot_notifications.h"
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
|
#include <shellapi.h>
|
||||||
|
#include <roapi.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr wchar_t ControllerClass[]=L"KSwitcher.Screenshot.Controller";
|
constexpr wchar_t ControllerClass[]=L"KSwitcher.Screenshot.Controller";
|
||||||
@@ -11,6 +14,8 @@ LRESULT CALLBACK controller(HWND window,UINT message,WPARAM wp,LPARAM lp)
|
|||||||
SetWindowLongPtrW(window,GWLP_USERDATA,(LONG_PTR)host);
|
SetWindowLongPtrW(window,GWLP_USERDATA,(LONG_PTR)host);
|
||||||
}
|
}
|
||||||
if(host && message==ks::ScreenshotRequest) {
|
if(host && message==ks::ScreenshotRequest) {
|
||||||
|
host->autoOpenEditor=(wp&1)!=0;
|
||||||
|
ks::setLoggingEnabled((wp&2)!=0);
|
||||||
try { ks::showScreenshot(*host); }
|
try { ks::showScreenshot(*host); }
|
||||||
catch(const std::exception &e) {
|
catch(const std::exception &e) {
|
||||||
MessageBoxW(window,ks::wide(e.what()).c_str(),L"Screen capture",MB_OK|MB_ICONERROR);
|
MessageBoxW(window,ks::wide(e.what()).c_str(),L"Screen capture",MB_OK|MB_ICONERROR);
|
||||||
@@ -18,6 +23,18 @@ LRESULT CALLBACK controller(HWND window,UINT message,WPARAM wp,LPARAM lp)
|
|||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
if(host && message==WM_COPYDATA) {
|
||||||
|
auto data=(COPYDATASTRUCT *)lp;
|
||||||
|
if(!data || data->dwData!=73 || data->cbData!=37*sizeof(wchar_t) || !data->lpData) return FALSE;
|
||||||
|
std::wstring token((const wchar_t *)data->lpData,36);
|
||||||
|
if(!ks::validScreenshotToken(token)) return FALSE;
|
||||||
|
try { ks::showScreenshotEditor(*host,ks::loadNotifiedScreenshot(token)); }
|
||||||
|
catch(const std::exception &e) {
|
||||||
|
MessageBoxW(window,ks::wide(e.what()).c_str(),L"Screenshot",MB_OK|MB_ICONINFORMATION);
|
||||||
|
PostMessageW(window,ks::ScreenshotIdle,0,0);
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
if(host && message==ks::ScreenshotIdle && !host->windows) {
|
if(host && message==ks::ScreenshotIdle && !host->windows) {
|
||||||
DestroyWindow(window);
|
DestroyWindow(window);
|
||||||
PostQuitMessage(0);
|
PostQuitMessage(0);
|
||||||
@@ -29,6 +46,33 @@ LRESULT CALLBACK controller(HWND window,UINT message,WPARAM wp,LPARAM lp)
|
|||||||
|
|
||||||
int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
||||||
{
|
{
|
||||||
|
std::wstring token;
|
||||||
|
bool autoOpenEditor=false, writeLog=false;
|
||||||
|
int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
|
||||||
|
if(argc>1 && std::wstring(argv[argc-1])==L"--write-log") {
|
||||||
|
writeLog=true; --argc;
|
||||||
|
ks::setLoggingEnabled(true);
|
||||||
|
}
|
||||||
|
if(argc>=2 && std::wstring(argv[1])==L"--toast-activated") {
|
||||||
|
LocalFree(argv); return ks::runScreenshotToastActivator();
|
||||||
|
}
|
||||||
|
if(argc==2 && std::wstring(argv[1])==L"--register-notifications") {
|
||||||
|
LocalFree(argv); CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
||||||
|
int result=0; try { ks::registerScreenshotNotifications(); } catch(...) { result=1; }
|
||||||
|
CoUninitialize(); return result;
|
||||||
|
}
|
||||||
|
if(argc==2 && (std::wstring(argv[1])==L"--auto-open-editor=0" ||
|
||||||
|
std::wstring(argv[1])==L"--auto-open-editor=1")) {
|
||||||
|
autoOpenEditor=std::wstring(argv[1])==L"--auto-open-editor=1";
|
||||||
|
} else if(argc>1) {
|
||||||
|
if(argc==3 && std::wstring(argv[1])==L"--open") {
|
||||||
|
std::wstring uri=argv[2]; const std::wstring prefix=L"kswitcher-screenshot:";
|
||||||
|
if(uri.starts_with(prefix)) token=uri.substr(prefix.size());
|
||||||
|
}
|
||||||
|
if(!ks::validScreenshotToken(token)) { LocalFree(argv); return 1; }
|
||||||
|
}
|
||||||
|
LocalFree(argv);
|
||||||
|
COPYDATASTRUCT open{73,DWORD((token.size()+1)*sizeof(wchar_t)),token.data()};
|
||||||
HANDLE mutex=CreateMutexW(nullptr,FALSE,L"Local\\KSwitcher.Screenshot.Singleton");
|
HANDLE mutex=CreateMutexW(nullptr,FALSE,L"Local\\KSwitcher.Screenshot.Singleton");
|
||||||
if(!mutex) return 1;
|
if(!mutex) return 1;
|
||||||
// Wait through startup/shutdown races; the controller accepts requests only while alive.
|
// Wait through startup/shutdown races; the controller accepts requests only while alive.
|
||||||
@@ -40,30 +84,36 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
|||||||
DWORD pid{}; GetWindowThreadProcessId(existing,&pid);
|
DWORD pid{}; GetWindowThreadProcessId(existing,&pid);
|
||||||
AllowSetForegroundWindow(pid);
|
AllowSetForegroundWindow(pid);
|
||||||
DWORD_PTR accepted{};
|
DWORD_PTR accepted{};
|
||||||
if(SendMessageTimeoutW(existing,ks::ScreenshotRequest,0,0,SMTO_ABORTIFHUNG,2000,&accepted) && accepted) {
|
if(SendMessageTimeoutW(existing,token.empty()?ks::ScreenshotRequest:WM_COPYDATA,token.empty()?(autoOpenEditor?1:0)|(writeLog?2:0):0,
|
||||||
|
token.empty()?0:(LPARAM)&open,SMTO_ABORTIFHUNG,2000,&accepted) && accepted) {
|
||||||
CloseHandle(mutex); return 0;
|
CloseHandle(mutex); return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Sleep(50);
|
Sleep(50);
|
||||||
}
|
}
|
||||||
if(!acquired) { CloseHandle(mutex); return 1; }
|
if(!acquired) { CloseHandle(mutex); return 1; }
|
||||||
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
RoInitialize(RO_INIT_SINGLETHREADED);
|
||||||
INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES|ICC_BAR_CLASSES};
|
INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES|ICC_BAR_CLASSES};
|
||||||
InitCommonControlsEx(&controls);
|
InitCommonControlsEx(&controls);
|
||||||
ks::ScreenshotHost host{instance};
|
ks::ScreenshotHost host{instance};
|
||||||
|
host.notify=ks::notifyScreenshot;
|
||||||
WNDCLASSW cls{}; cls.hInstance=instance; cls.lpfnWndProc=controller; cls.lpszClassName=ControllerClass;
|
WNDCLASSW cls{}; cls.hInstance=instance; cls.lpfnWndProc=controller; cls.lpszClassName=ControllerClass;
|
||||||
RegisterClassW(&cls);
|
RegisterClassW(&cls);
|
||||||
host.window=CreateWindowW(ControllerClass,L"KSwitcher Screenshot",0,0,0,0,0,nullptr,nullptr,instance,&host);
|
host.window=CreateWindowW(ControllerClass,L"KSwitcher Screenshot",0,0,0,0,0,nullptr,nullptr,instance,&host);
|
||||||
int result=1;
|
int result=1;
|
||||||
if(host.window) {
|
if(host.window) {
|
||||||
PostMessageW(host.window,ks::ScreenshotRequest,0,0);
|
// Toast protocol activation may run unelevated while the editor was launched by an elevated resident.
|
||||||
|
// WM_COPYDATA accepts only a validated cache GUID, never a path or executable command.
|
||||||
|
ChangeWindowMessageFilterEx(host.window,WM_COPYDATA,MSGFLT_ALLOW,nullptr);
|
||||||
|
if(token.empty()) PostMessageW(host.window,ks::ScreenshotRequest,(autoOpenEditor?1:0)|(writeLog?2:0),0);
|
||||||
|
else SendMessageW(host.window,WM_COPYDATA,0,(LPARAM)&open);
|
||||||
MSG message{};
|
MSG message{};
|
||||||
while((result=GetMessageW(&message,nullptr,0,0))>0) {
|
while((result=GetMessageW(&message,nullptr,0,0))>0) {
|
||||||
TranslateMessage(&message); DispatchMessageW(&message);
|
TranslateMessage(&message); DispatchMessageW(&message);
|
||||||
}
|
}
|
||||||
result=result<0?1:0;
|
result=result<0?1:0;
|
||||||
}
|
}
|
||||||
CoUninitialize();
|
RoUninitialize();
|
||||||
ReleaseMutex(mutex); CloseHandle(mutex);
|
ReleaseMutex(mutex); CloseHandle(mutex);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
#include "screenshot_notifications.h"
|
||||||
|
#include "ocr.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <aclapi.h>
|
||||||
|
#include <sddl.h>
|
||||||
|
#include <shlobj.h>
|
||||||
|
#include <propkey.h>
|
||||||
|
#include <propvarutil.h>
|
||||||
|
#include <roapi.h>
|
||||||
|
#include <winstring.h>
|
||||||
|
#include <wincodec.h>
|
||||||
|
#ifdef __MINGW32__
|
||||||
|
// This SDK aliases BYTE and boolean, producing duplicate template specializations.
|
||||||
|
// Notifications use the boolean specialization; IReference<BYTE> is unused here.
|
||||||
|
#define ____FIReference_1_BYTE_INTERFACE_DEFINED__
|
||||||
|
#endif
|
||||||
|
#include <windows.ui.notifications.h>
|
||||||
|
#include <windows.data.xml.dom.h>
|
||||||
|
#include <wrl/client.h>
|
||||||
|
|
||||||
|
namespace ks {
|
||||||
|
namespace {
|
||||||
|
using Microsoft::WRL::ComPtr;
|
||||||
|
namespace Notifications=ABI::Windows::UI::Notifications;
|
||||||
|
namespace Xml=ABI::Windows::Data::Xml::Dom;
|
||||||
|
constexpr wchar_t AppId[]=L"HrynCo.KSwitcher.Screenshot";
|
||||||
|
std::string notificationError;
|
||||||
|
void check(HRESULT result) { if(FAILED(result)) { char message[100]; snprintf(message,sizeof(message),"Windows screenshot notification failed (0x%08lx)",(unsigned long)result); throw std::runtime_error(message); } }
|
||||||
|
struct String {
|
||||||
|
HSTRING value{};
|
||||||
|
explicit String(const std::wstring &s) { check(WindowsCreateString(s.c_str(),(UINT32)s.size(),&value)); }
|
||||||
|
~String() { WindowsDeleteString(value); }
|
||||||
|
};
|
||||||
|
std::filesystem::path cacheDirectory()
|
||||||
|
{
|
||||||
|
PWSTR folder{}; check(SHGetKnownFolderPath(FOLDERID_LocalAppData,0,nullptr,&folder));
|
||||||
|
auto path=std::filesystem::path(folder)/L"KSwitcher"/L"ScreenshotNotifications";
|
||||||
|
CoTaskMemFree(folder); std::filesystem::create_directories(path); return path;
|
||||||
|
}
|
||||||
|
std::filesystem::path previewDirectory()
|
||||||
|
{
|
||||||
|
HANDLE token{};
|
||||||
|
if(!OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY,&token)) check(HRESULT_FROM_WIN32(GetLastError()));
|
||||||
|
DWORD size{}; GetTokenInformation(token,TokenUser,nullptr,0,&size);
|
||||||
|
std::vector<BYTE> data(size);
|
||||||
|
BOOL read=GetTokenInformation(token,TokenUser,data.data(),size,&size);
|
||||||
|
DWORD error=read?ERROR_SUCCESS:GetLastError(); CloseHandle(token); check(HRESULT_FROM_WIN32(error));
|
||||||
|
auto user=reinterpret_cast<TOKEN_USER *>(data.data())->User.Sid;
|
||||||
|
LPWSTR sidText{}; if(!ConvertSidToStringSidW(user,&sidText)) check(HRESULT_FROM_WIN32(GetLastError()));
|
||||||
|
std::wstring sid(sidText); LocalFree(sidText);
|
||||||
|
PWSTR folder{}; check(SHGetKnownFolderPath(FOLDERID_ProgramData,0,nullptr,&folder));
|
||||||
|
auto path=std::filesystem::path(folder)/(L"KSwitcher.ScreenshotPreviews-"+sid); CoTaskMemFree(folder);
|
||||||
|
// The notification broker cannot load previews from some profile directories.
|
||||||
|
// Keep this cache outside the profile, private to the current user and SYSTEM.
|
||||||
|
auto sddl=L"O:"+sid+L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;"+sid+L")";
|
||||||
|
PSECURITY_DESCRIPTOR security{};
|
||||||
|
if(!ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(),SDDL_REVISION_1,&security,nullptr))
|
||||||
|
check(HRESULT_FROM_WIN32(GetLastError()));
|
||||||
|
SECURITY_ATTRIBUTES attributes{sizeof(attributes),security,FALSE};
|
||||||
|
BOOL created=CreateDirectoryW(path.c_str(),&attributes);
|
||||||
|
error=created?ERROR_SUCCESS:GetLastError();
|
||||||
|
LocalFree(security);
|
||||||
|
if(error!=ERROR_ALREADY_EXISTS) check(HRESULT_FROM_WIN32(error));
|
||||||
|
// Do not use a pre-created directory or junction controlled by another user.
|
||||||
|
DWORD flags=GetFileAttributesW(path.c_str());
|
||||||
|
if(flags==INVALID_FILE_ATTRIBUTES || !(flags&FILE_ATTRIBUTE_DIRECTORY) || (flags&FILE_ATTRIBUTE_REPARSE_POINT))
|
||||||
|
throw std::runtime_error("Invalid screenshot preview directory");
|
||||||
|
PSID owner{}; PSECURITY_DESCRIPTOR existing{};
|
||||||
|
error=GetNamedSecurityInfoW(const_cast<wchar_t *>(path.c_str()),SE_FILE_OBJECT,OWNER_SECURITY_INFORMATION,
|
||||||
|
&owner,nullptr,nullptr,nullptr,&existing);
|
||||||
|
check(HRESULT_FROM_WIN32(error));
|
||||||
|
bool owned=EqualSid(owner,user); LocalFree(existing);
|
||||||
|
if(!owned) throw std::runtime_error("Screenshot preview directory belongs to another user");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
void cleanCache(const std::filesystem::path &path)
|
||||||
|
{
|
||||||
|
std::vector<std::filesystem::directory_entry> files;
|
||||||
|
for(const auto &file:std::filesystem::directory_iterator(path))
|
||||||
|
if(file.is_regular_file() && validScreenshotToken(file.path().stem().wstring()) &&
|
||||||
|
(file.path().extension()==L".capture" || file.path().extension()==L".png")) files.push_back(file);
|
||||||
|
std::sort(files.begin(),files.end(),[](const auto &a,const auto &b){return a.last_write_time()>b.last_write_time();});
|
||||||
|
uintmax_t size=0;
|
||||||
|
for(size_t i=0;i<files.size();++i) {
|
||||||
|
size+=files[i].file_size();
|
||||||
|
if(i>=40 || size>256*1024*1024 || std::filesystem::file_time_type::clock::now()-files[i].last_write_time()>std::chrono::hours(24)) {
|
||||||
|
std::error_code ignored; std::filesystem::remove(files[i].path(),ignored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void savePreview(const Clip &clip,const std::filesystem::path &path)
|
||||||
|
{
|
||||||
|
auto pixels=decodeImage(clip,360,220);
|
||||||
|
for(size_t i=3;i<pixels.bgra.size();i+=4) pixels.bgra[i]=255;
|
||||||
|
ComPtr<IWICImagingFactory> factory; check(CoCreateInstance(CLSID_WICImagingFactory,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&factory)));
|
||||||
|
ComPtr<IWICStream> stream; check(factory->CreateStream(&stream)); check(stream->InitializeFromFilename(path.c_str(),GENERIC_WRITE));
|
||||||
|
ComPtr<IWICBitmapEncoder> encoder; check(factory->CreateEncoder(GUID_ContainerFormatPng,nullptr,&encoder));
|
||||||
|
check(encoder->Initialize(stream.Get(),WICBitmapEncoderNoCache));
|
||||||
|
ComPtr<IWICBitmapFrameEncode> frame; check(encoder->CreateNewFrame(&frame,nullptr)); check(frame->Initialize(nullptr));
|
||||||
|
check(frame->SetSize(pixels.width,pixels.height));
|
||||||
|
auto format=GUID_WICPixelFormat32bppBGRA; check(frame->SetPixelFormat(&format));
|
||||||
|
if(format!=GUID_WICPixelFormat32bppBGRA) throw std::runtime_error("Unsupported preview pixel format");
|
||||||
|
check(frame->WritePixels(pixels.height,pixels.width*4,(UINT)pixels.bgra.size(),pixels.bgra.data()));
|
||||||
|
check(frame->Commit()); check(encoder->Commit());
|
||||||
|
}
|
||||||
|
std::wstring escapeXml(const std::wstring &s)
|
||||||
|
{
|
||||||
|
std::wstring result;
|
||||||
|
for(auto ch:s) { if(ch==L'&') result+=L"&"; else if(ch==L'\"') result+=L"""; else if(ch==L'<') result+=L"<"; else result+=ch; }
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Clip loadNotifiedScreenshot(const std::wstring &id)
|
||||||
|
{
|
||||||
|
if(!validScreenshotToken(id)) throw std::runtime_error("Invalid screenshot notification");
|
||||||
|
auto directory=cacheDirectory(); cleanCache(directory);
|
||||||
|
auto path=directory/(id+L".capture");
|
||||||
|
if(!std::filesystem::exists(path)) throw std::runtime_error("This screenshot notification has expired. Take a new screenshot.");
|
||||||
|
return Clip{{{CF_DIB,L"",unprotect(readFile(path,160*1024*1024))}}};
|
||||||
|
}
|
||||||
|
bool notifyScreenshot(const Clip &clip)
|
||||||
|
{
|
||||||
|
notificationError.clear();
|
||||||
|
const char *stage="registration";
|
||||||
|
try {
|
||||||
|
registerScreenshotNotifications();
|
||||||
|
check(SetCurrentProcessExplicitAppUserModelID(AppId));
|
||||||
|
stage="notification manager";
|
||||||
|
ComPtr<Notifications::IToastNotificationManagerStatics> manager;
|
||||||
|
String managerClass(L"Windows.UI.Notifications.ToastNotificationManager");
|
||||||
|
check(RoGetActivationFactory(managerClass.value,IID_PPV_ARGS(&manager)));
|
||||||
|
ComPtr<Notifications::IToastNotifier> notifier; String appId(AppId);
|
||||||
|
stage="notifier";
|
||||||
|
check(manager->CreateToastNotifierWithId(appId.value,¬ifier));
|
||||||
|
stage="settings";
|
||||||
|
Notifications::NotificationSetting setting{};
|
||||||
|
auto settingResult=notifier->get_Setting(&setting);
|
||||||
|
// A first-time sender has no settings record until its first notification.
|
||||||
|
if(settingResult!=HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) check(settingResult);
|
||||||
|
if(setting!=Notifications::NotificationSetting_Enabled) throw std::runtime_error("Screenshot notifications are disabled in Windows settings");
|
||||||
|
stage="cache";
|
||||||
|
auto directory=cacheDirectory(); cleanCache(directory);
|
||||||
|
GUID guid{}; check(CoCreateGuid(&guid)); wchar_t value[40]{}; StringFromGUID2(guid,value,40);
|
||||||
|
std::wstring id(value+1,36);
|
||||||
|
auto dib=std::find_if(clip.formats.begin(),clip.formats.end(),[](const auto &format){return format.id==CF_DIB;});
|
||||||
|
if(dib==clip.formats.end() || dib->data.size()>128*1024*1024) return false;
|
||||||
|
atomicWrite(directory/(id+L".capture"),protect(dib->data));
|
||||||
|
stage="preview";
|
||||||
|
auto previews=previewDirectory(); cleanCache(previews);
|
||||||
|
auto preview=previews/(id+L".png"); savePreview(clip,preview);
|
||||||
|
cleanCache(previews);
|
||||||
|
cleanCache(directory);
|
||||||
|
stage="XML";
|
||||||
|
String documentClass(L"Windows.Data.Xml.Dom.XmlDocument"); ComPtr<IInspectable> documentInstance;
|
||||||
|
check(RoActivateInstance(documentClass.value,&documentInstance));
|
||||||
|
ComPtr<Xml::IXmlDocument> document; check(documentInstance.As(&document));
|
||||||
|
ComPtr<Xml::IXmlDocumentIO> documentIO; check(document.As(&documentIO));
|
||||||
|
auto uri=L"file:///"+preview.generic_wstring();
|
||||||
|
String content(L"<toast launch=\""+id+L"\"><visual><binding template=\"ToastGeneric\">"
|
||||||
|
L"<text>Screenshot copied</text><text>Click to edit and annotate</text><image id=\"1\" placement=\"appLogoOverride\" alt=\"Screenshot preview\" src=\""+escapeXml(uri)+L"\"/></binding></visual><audio silent=\"true\"/></toast>");
|
||||||
|
check(documentIO->LoadXml(content.value));
|
||||||
|
stage="toast factory";
|
||||||
|
ComPtr<Notifications::IToastNotificationFactory> toastFactory; String toastClass(L"Windows.UI.Notifications.ToastNotification");
|
||||||
|
check(RoGetActivationFactory(toastClass.value,IID_PPV_ARGS(&toastFactory)));
|
||||||
|
ComPtr<Notifications::IToastNotification> toast; check(toastFactory->CreateToastNotification(document.Get(),&toast));
|
||||||
|
ComPtr<ABI::Windows::Foundation::IPropertyValueStatics> values;
|
||||||
|
String propertyClass(L"Windows.Foundation.PropertyValue");
|
||||||
|
check(RoGetActivationFactory(propertyClass.value,IID_PPV_ARGS(&values)));
|
||||||
|
FILETIME now{}; GetSystemTimeAsFileTime(&now);
|
||||||
|
ULARGE_INTEGER timestamp{}; timestamp.LowPart=now.dwLowDateTime; timestamp.HighPart=now.dwHighDateTime;
|
||||||
|
ABI::Windows::Foundation::DateTime expires{(INT64)(timestamp.QuadPart+24ull*60*60*10000000)};
|
||||||
|
ComPtr<IInspectable> boxed; check(values->CreateDateTime(expires,&boxed));
|
||||||
|
ComPtr<ABI::Windows::Foundation::IReference<ABI::Windows::Foundation::DateTime>> expiration;
|
||||||
|
check(boxed.As(&expiration)); check(toast->put_ExpirationTime(expiration.Get()));
|
||||||
|
stage="show";
|
||||||
|
check(notifier->Show(toast.Get()));
|
||||||
|
return true;
|
||||||
|
} catch(const std::exception &e) { notificationError=std::string(stage)+": "+e.what(); log(wide(notificationError)); return false; } // Clipboard is already populated; fall back to the editor.
|
||||||
|
}
|
||||||
|
std::string screenshotNotificationError() { return notificationError; }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "platform.h"
|
||||||
|
namespace ks {
|
||||||
|
bool notifyScreenshot(const Clip &);
|
||||||
|
std::string screenshotNotificationError();
|
||||||
|
void registerScreenshotNotifications();
|
||||||
|
int runScreenshotToastActivator();
|
||||||
|
DWORD startScreenshotActivationListener();
|
||||||
|
bool validScreenshotToken(const std::wstring &);
|
||||||
|
Clip loadNotifiedScreenshot(const std::wstring &);
|
||||||
|
}
|
||||||
+3
-1
@@ -151,6 +151,7 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
|||||||
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);
|
s.screenshot = str(j, "screenshotHotkey", s.screenshot);
|
||||||
|
s.screenshotAutoOpenEditor = j.value("screenshotAutoOpenEditor", false);
|
||||||
if (j.contains("layouts"))
|
if (j.contains("layouts"))
|
||||||
{
|
{
|
||||||
s.cycle.clear();
|
s.cycle.clear();
|
||||||
@@ -205,7 +206,8 @@ Bytes encodeSettings(const Settings &s)
|
|||||||
{"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)}};
|
{"screenshotHotkey", utf8(s.screenshot)},
|
||||||
|
{"screenshotAutoOpenEditor", s.screenshotAutoOpenEditor}};
|
||||||
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));
|
||||||
|
|||||||
+22
-11
@@ -31,7 +31,7 @@ HFONT font(HWND w)
|
|||||||
struct WindowLayout
|
struct WindowLayout
|
||||||
{
|
{
|
||||||
int width{}, height{}, x{}, y{};
|
int width{}, height{}, x{}, y{};
|
||||||
bool scrolling{}, arranging{};
|
bool scrolling{}, arranging{}, history{};
|
||||||
int wheelRemainder{}, horizontalWheelRemainder{};
|
int wheelRemainder{}, horizontalWheelRemainder{};
|
||||||
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
||||||
};
|
};
|
||||||
@@ -68,9 +68,18 @@ void arrangeWindow(HWND w)
|
|||||||
layout->x = SetScrollInfo(w, SB_HORZ, &horizontal, TRUE);
|
layout->x = SetScrollInfo(w, SB_HORZ, &horizontal, TRUE);
|
||||||
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
||||||
}
|
}
|
||||||
|
RECT client{}; GetClientRect(w, &client);
|
||||||
|
int extraWidth = layout->history ? std::max(0, int(client.right) - scale(w, layout->width)) : 0;
|
||||||
|
int extraHeight = layout->history ? std::max(0, int(client.bottom) - scale(w, layout->height)) : 0;
|
||||||
for (const auto &[child, r] : layout->controls)
|
for (const auto &[child, r] : layout->controls)
|
||||||
SetWindowPos(child, nullptr, scale(w, r.left) - layout->x, scale(w, r.top) - layout->y,
|
{
|
||||||
scale(w, r.right - r.left), scale(w, r.bottom - r.top), SWP_NOACTIVATE | SWP_NOZORDER);
|
int id = GetDlgCtrlID(child);
|
||||||
|
SetWindowPos(child, nullptr, scale(w, r.left) - layout->x,
|
||||||
|
scale(w, r.top) - layout->y + (id >= 110 && id <= 114 ? extraHeight : 0),
|
||||||
|
scale(w, r.right - r.left) + (id == 101 || id == 102 ? extraWidth : 0),
|
||||||
|
scale(w, r.bottom - r.top) + (id == 102 ? extraHeight : 0),
|
||||||
|
SWP_NOACTIVATE | SWP_NOZORDER);
|
||||||
|
}
|
||||||
layout->arranging = false;
|
layout->arranging = false;
|
||||||
InvalidateRect(w, nullptr, TRUE);
|
InvalidateRect(w, nullptr, TRUE);
|
||||||
}
|
}
|
||||||
@@ -351,6 +360,9 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
|||||||
SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)next);
|
SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)next);
|
||||||
if (old)
|
if (old)
|
||||||
DeleteObject(old);
|
DeleteObject(old);
|
||||||
|
// layoutProc consumes WM_DPICHANGED, so owner-drawn rows must be resized here.
|
||||||
|
if (layout && layout->history)
|
||||||
|
SendDlgItemMessageW(w, 102, LB_SETITEMHEIGHT, 0, scale(w, 88));
|
||||||
fitToMonitor(w, *(RECT *)lp);
|
fitToMonitor(w, *(RECT *)lp);
|
||||||
arrangeWindow(w);
|
arrangeWindow(w);
|
||||||
}
|
}
|
||||||
@@ -738,12 +750,6 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
v->pending.clear();
|
v->pending.clear();
|
||||||
++v->generation;
|
++v->generation;
|
||||||
return 0;
|
return 0;
|
||||||
case WM_DPICHANGED:
|
|
||||||
{
|
|
||||||
resizeForDpi(w, wp, lp);
|
|
||||||
SendDlgItemMessageW(w, 102, LB_SETITEMHEIGHT, 0, scale(w, 88));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
case WM_NCDESTROY:
|
case WM_NCDESTROY:
|
||||||
disposeFont(w);
|
disposeFont(w);
|
||||||
v->app->historyWindow = nullptr;
|
v->app->historyWindow = nullptr;
|
||||||
@@ -813,6 +819,7 @@ void fillSettings(HWND w, SettingsView *v)
|
|||||||
check(w, 201, s.recent);
|
check(w, 201, s.recent);
|
||||||
check(w, 202, s.switchLayout);
|
check(w, 202, s.switchLayout);
|
||||||
check(w, 208, s.writeLog);
|
check(w, 208, s.writeLog);
|
||||||
|
check(w, 209, s.screenshotAutoOpenEditor);
|
||||||
check(w, 203, s.history);
|
check(w, 203, s.history);
|
||||||
check(w, 204, s.autostart);
|
check(w, 204, s.autostart);
|
||||||
check(w, 206, s.elevatedAutostart);
|
check(w, 206, s.elevatedAutostart);
|
||||||
@@ -863,6 +870,7 @@ Settings collectSettings(HWND w, SettingsView *v)
|
|||||||
s.recent = checked(w, 201);
|
s.recent = checked(w, 201);
|
||||||
s.switchLayout = checked(w, 202);
|
s.switchLayout = checked(w, 202);
|
||||||
s.writeLog = checked(w, 208);
|
s.writeLog = checked(w, 208);
|
||||||
|
s.screenshotAutoOpenEditor = checked(w, 209);
|
||||||
s.history = checked(w, 203);
|
s.history = checked(w, 203);
|
||||||
s.autostart = checked(w, 204);
|
s.autostart = checked(w, 204);
|
||||||
s.elevatedAutostart = checked(w, 206);
|
s.elevatedAutostart = checked(w, 206);
|
||||||
@@ -1325,7 +1333,7 @@ void refreshHistory(App &app)
|
|||||||
search(app.historyWindow, v);
|
search(app.historyWindow, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void showHistory(App &app)
|
void showHistory(App &app, bool visible)
|
||||||
{
|
{
|
||||||
if (!app.history)
|
if (!app.history)
|
||||||
{
|
{
|
||||||
@@ -1333,7 +1341,7 @@ void showHistory(App &app)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HWND foreground = GetForegroundWindow();
|
HWND foreground = GetForegroundWindow();
|
||||||
if (app.historyWindow && IsWindowVisible(app.historyWindow) && foreground == app.historyWindow)
|
if (visible && app.historyWindow && IsWindowVisible(app.historyWindow) && foreground == app.historyWindow)
|
||||||
{
|
{
|
||||||
auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA);
|
auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA);
|
||||||
if (v->items.size() > 1)
|
if (v->items.size() > 1)
|
||||||
@@ -1346,6 +1354,7 @@ void showHistory(App &app)
|
|||||||
{
|
{
|
||||||
HWND w = makeWindow(app, L"KSwitcher.History", L"Clipboard history", historyProc, 760, 500);
|
HWND w = makeWindow(app, L"KSwitcher.History", L"Clipboard history", historyProc, 760, 500);
|
||||||
app.historyWindow = w;
|
app.historyWindow = w;
|
||||||
|
windowLayoutState(w)->history = true;
|
||||||
((HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA))->rich = createRichText(w, 105, false);
|
((HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA))->rich = createRichText(w, 105, false);
|
||||||
label(w, L"Search clipboard history", 16, 12, 500);
|
label(w, L"Search clipboard history", 16, 12, 500);
|
||||||
edit(w, 101, 16, 38, 728);
|
edit(w, 101, 16, 38, 728);
|
||||||
@@ -1362,6 +1371,7 @@ void showHistory(App &app)
|
|||||||
SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w);
|
SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w);
|
||||||
SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w);
|
SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w);
|
||||||
}
|
}
|
||||||
|
if (!visible) return;
|
||||||
POINT cursor;
|
POINT cursor;
|
||||||
GetCursorPos(&cursor);
|
GetCursorPos(&cursor);
|
||||||
MONITORINFO monitor{sizeof(monitor)};
|
MONITORINFO monitor{sizeof(monitor)};
|
||||||
@@ -1651,6 +1661,7 @@ void showSettings(App &app, bool visible)
|
|||||||
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"Open editor after screen capture", 209, 420, 224, 340);
|
||||||
checkbox(w, L"Automatically correct layout (experimental)", 207, 18, 210, 395);
|
checkbox(w, L"Automatically correct layout (experimental)", 207, 18, 210, 395);
|
||||||
label(w, L"Layout cycle — top to bottom", 18, 240, 400);
|
label(w, L"Layout cycle — top to bottom", 18, 240, 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,
|
||||||
|
|||||||
+2
-1
@@ -2,8 +2,9 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.5.0";
|
inline constexpr wchar_t AppVersion[] = L"0.6.3";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
struct UpdateResult
|
struct UpdateResult
|
||||||
{
|
{
|
||||||
std::wstring state, version, error;
|
std::wstring state, version, error;
|
||||||
|
|||||||
+45
-1
@@ -7,6 +7,20 @@
|
|||||||
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'}}}; }
|
||||||
|
static bool updateTimer(App &a) {
|
||||||
|
a.updateRunning=true; // Exercise timer scheduling without launching a network request.
|
||||||
|
App::windowProc(a.window,WM_TIMER,5,0);
|
||||||
|
bool first=KillTimer(a.window,5)!=FALSE;
|
||||||
|
App::windowProc(a.window,WM_TIMER,5,0);
|
||||||
|
bool repeated=KillTimer(a.window,5)!=FALSE;
|
||||||
|
a.updateRunning=false;
|
||||||
|
return first && repeated && !a.updateWorker.joinable();
|
||||||
|
}
|
||||||
|
static std::wstring updateResult(App &a,const wchar_t *state,const wchar_t *version) {
|
||||||
|
auto result=new UpdateResult; result->state=state; result->version=version;
|
||||||
|
App::windowProc(a.window,UpdateMessage,0,(LPARAM)result);
|
||||||
|
return a.notifiedUpdate;
|
||||||
|
}
|
||||||
static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); }
|
static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -31,6 +45,12 @@ 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");
|
||||||
|
require(ks::UpdateCheckIntervalMs==21600000,"automatic update interval is six hours");
|
||||||
|
require(ks::InputRegressionAccess::updateTimer(app),"update timer rearms and avoids overlapping checks");
|
||||||
|
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.0")==L"99.0.0","background update records announced version");
|
||||||
|
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.0")==L"99.0.0","same available version remains deduplicated");
|
||||||
|
require(ks::InputRegressionAccess::updateResult(app,L"error",L"")==L"99.0.0","network error preserves announced version");
|
||||||
|
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.1")==L"99.0.1","new version can be announced after an error");
|
||||||
for(int variant=0;variant<3;++variant) for(int icon=0;icon<15;++icon)
|
for(int variant=0;variant<3;++variant) for(int icon=0;icon<15;++icon)
|
||||||
for(int size:{22,44,55}) {
|
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);
|
auto handle=(HICON)LoadImageW(app.instance,MAKEINTRESOURCEW(icon<9?300+variant*10+icon:400+variant*10+icon-9),IMAGE_ICON,size,size,0);
|
||||||
@@ -485,9 +505,33 @@ int main(int argc, char **argv) {
|
|||||||
fitted=ks::fitWindowRect({2000,2000,2600,2400},{-1920,0,0,1080});
|
fitted=ks::fitWindowRect({2000,2000,2600,2400},{-1920,0,0,1080});
|
||||||
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
|
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
|
||||||
ks::showSettings(app,false);
|
ks::showSettings(app,false);
|
||||||
|
if (!app.history) app.history=std::make_unique<ks::History>(temp/L"history-layout");
|
||||||
|
ks::showHistory(app,false);
|
||||||
|
auto history=app.historyWindow;
|
||||||
|
require(history&&!IsWindowVisible(history),"history layout tested without changing focus");
|
||||||
|
auto childRect=[&](int id) { RECT r{}; GetWindowRect(GetDlgItem(history,id),&r); MapWindowPoints(nullptr,history,(POINT*)&r,2); return r; };
|
||||||
|
RECT historyFrame{}; GetWindowRect(history,&historyFrame);
|
||||||
|
auto listBefore=childRect(102),searchBefore=childRect(101),buttonBefore=childRect(110);
|
||||||
|
SetWindowPos(history,nullptr,0,0,historyFrame.right-historyFrame.left+120,historyFrame.bottom-historyFrame.top+180,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||||
|
auto listAfter=childRect(102),searchAfter=childRect(101),buttonAfter=childRect(110);
|
||||||
|
require(listAfter.right-listBefore.right==120 && listAfter.bottom-listBefore.bottom==180,"history list fills added width and height");
|
||||||
|
require(searchAfter.right-searchBefore.right==120 && searchAfter.top==searchBefore.top,"history search grows horizontally only");
|
||||||
|
require(buttonAfter.top-buttonBefore.top==180 && buttonAfter.bottom-listAfter.bottom==buttonBefore.bottom-listBefore.bottom,"history actions stay below list after resize");
|
||||||
|
SetWindowPos(history,nullptr,0,0,historyFrame.right-historyFrame.left,historyFrame.bottom-historyFrame.top,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
|
||||||
|
require(EqualRect(&listBefore,&(listAfter=childRect(102))),"history resize roundtrip restores geometry");
|
||||||
|
// Reproduce a row height left over from another DPI without changing desktop settings.
|
||||||
|
UINT historyDpi=GetDpiForWindow(history);
|
||||||
|
for (int staleDpi : {96, 144, 192, 240}) {
|
||||||
|
SendDlgItemMessageW(history,102,LB_SETITEMHEIGHT,0,MulDiv(88,staleDpi,96));
|
||||||
|
SendMessageW(history,WM_DPICHANGED,MAKELONG(historyDpi,historyDpi),(LPARAM)&historyFrame);
|
||||||
|
require(SendDlgItemMessageW(history,102,LB_GETITEMHEIGHT,0,0)==MulDiv(88,historyDpi,96),
|
||||||
|
"DPI change updates owner-drawn history row height through layout subclass");
|
||||||
|
require(EqualRect(&listBefore,&(listAfter=childRect(102))),"DPI refresh preserves history list geometry");
|
||||||
|
}
|
||||||
auto settings=app.settingsWindow;
|
auto settings=app.settingsWindow;
|
||||||
|
require(SendDlgItemMessageW(settings,209,BM_GETCHECK,0,0)==BST_UNCHECKED,"automatic screenshot editor checkbox defaults off");
|
||||||
require(settings&&!IsWindowVisible(settings),"settings layout tested without changing user focus");
|
require(settings&&!IsWindowVisible(settings),"settings layout tested without changing user focus");
|
||||||
for(auto pair:{std::pair{207,215},std::pair{207,232},std::pair{207,220},std::pair{232,230},std::pair{230,231},std::pair{231,253}}) {
|
for(auto pair:{std::pair{209,215},std::pair{209,207},std::pair{209,220},std::pair{209,232},std::pair{207,215},std::pair{207,232},std::pair{207,220},std::pair{232,230},std::pair{230,231},std::pair{231,253}}) {
|
||||||
RECT first{},second{},overlap{};
|
RECT first{},second{},overlap{};
|
||||||
GetWindowRect(GetDlgItem(settings,pair.first),&first); GetWindowRect(GetDlgItem(settings,pair.second),&second);
|
GetWindowRect(GetDlgItem(settings,pair.first),&first); GetWindowRect(GetDlgItem(settings,pair.second),&second);
|
||||||
require(!IntersectRect(&overlap,&first,&second),"capture hotkey and layout controls do not overlap");
|
require(!IntersectRect(&overlap,&first,&second),"capture hotkey and layout controls do not overlap");
|
||||||
|
|||||||
@@ -35,30 +35,35 @@ function Select-Region($Window) {
|
|||||||
Send $Window 0x201 1 ((40 -shl 16) -bor 40)
|
Send $Window 0x201 1 ((40 -shl 16) -bor 40)
|
||||||
Send $Window 0x200 1 ((240 -shl 16) -bor 360)
|
Send $Window 0x200 1 ((240 -shl 16) -bor 360)
|
||||||
Send $Window 0x202 0 ((240 -shl 16) -bor 360)
|
Send $Window 0x202 0 ((240 -shl 16) -bor 360)
|
||||||
Wait-For { [ScreenshotProcessTest]::Title($Window) -match 'Screenshot #' } 'Selection did not become an editor.'
|
Wait-For { (Capture-Windows) -notcontains $Window } 'Selection did not close after copying (notifications may be disabled).'
|
||||||
}
|
}
|
||||||
function Close-Editor($Window) {
|
function Close-Editor($Window) {
|
||||||
Send $Window 0x10
|
Send $Window 0x10
|
||||||
Wait-For { @([ScreenshotProcessTest]::Windows($script:process.Id,'#32770')).Count -eq 1 } 'Missing close confirmation.'
|
Wait-For { (Capture-Windows) -notcontains $Window } 'Unedited copied screenshot did not close.'
|
||||||
$dialog=[ScreenshotProcessTest]::Windows($script:process.Id,'#32770')[0]
|
}
|
||||||
Send $dialog (0x400+102) 6 # TDM_CLICK_BUTTON / IDYES; no clipboard writes.
|
function Latest-Token {
|
||||||
Wait-For { (Capture-Windows) -notcontains $Window } 'Editor did not close.'
|
(Get-ChildItem -LiteralPath (Join-Path $env:LOCALAPPDATA 'KSwitcher/ScreenshotNotifications') -Filter '*.capture' | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1).BaseName
|
||||||
}
|
}
|
||||||
$process=$null
|
$process=$null
|
||||||
try {
|
try {
|
||||||
$process=Start-Process -FilePath $Executable -WindowStyle Hidden -PassThru
|
$process=Start-Process -FilePath $Executable -ArgumentList @("--auto-open-editor=0") -WindowStyle Hidden -PassThru
|
||||||
Wait-For { (Capture-Windows).Count -eq 1 } 'First selection did not open.'
|
Wait-For { (Capture-Windows).Count -eq 1 } 'First selection did not open.'
|
||||||
$first=(Capture-Windows)[0]; Select-Region $first
|
Select-Region (Capture-Windows)[0]
|
||||||
$secondLaunch=Start-Process -FilePath $Executable -WindowStyle Hidden -PassThru
|
if (!$process.WaitForExit(5000)) { throw 'Process remained alive after notification.' }
|
||||||
if (!$secondLaunch.WaitForExit(10000) -or $secondLaunch.ExitCode -ne 0) { throw 'Second launch did not hand off.' }
|
$token=Latest-Token
|
||||||
Wait-For { (Capture-Windows).Count -eq 2 } 'Second selection did not open in same process.'
|
$process=Start-Process -FilePath $Executable -ArgumentList @('--open',"kswitcher-screenshot:$token") -WindowStyle Hidden -PassThru
|
||||||
$second=(Capture-Windows | Where-Object { $_ -ne $first })[0]; Select-Region $second
|
Wait-For { (Capture-Windows).Count -eq 1 } 'Notification activation did not open an editor.'
|
||||||
|
$first=(Capture-Windows)[0]
|
||||||
|
$secondLaunch=Start-Process -FilePath $Executable -ArgumentList @('--open',"kswitcher-screenshot:$token") -WindowStyle Hidden -PassThru
|
||||||
|
if (!$secondLaunch.WaitForExit(10000) -or $secondLaunch.ExitCode -ne 0) { throw 'Second activation did not hand off.' }
|
||||||
|
Wait-For { (Capture-Windows).Count -eq 2 } 'Second editor did not open in same process.'
|
||||||
|
$second=(Capture-Windows | Where-Object { $_ -ne $first })[0]
|
||||||
if ([ScreenshotProcessTest]::Title($first) -eq [ScreenshotProcessTest]::Title($second)) { throw 'Editor indices collide.' }
|
if ([ScreenshotProcessTest]::Title($first) -eq [ScreenshotProcessTest]::Title($second)) { throw 'Editor indices collide.' }
|
||||||
Close-Editor $first
|
Close-Editor $first
|
||||||
if ($process.HasExited) { throw 'Process exited with an editor open.' }
|
if ($process.HasExited) { throw 'Process exited with an editor open.' }
|
||||||
Close-Editor $second
|
Close-Editor $second
|
||||||
if (!$process.WaitForExit(5000) -or $process.ExitCode -ne 0) { throw 'Last editor did not exit process.' }
|
if (!$process.WaitForExit(5000) -or $process.ExitCode -ne 0) { throw 'Last editor did not exit process.' }
|
||||||
$process=Start-Process -FilePath $Executable -WindowStyle Hidden -PassThru
|
$process=Start-Process -FilePath $Executable -ArgumentList @("--auto-open-editor=0") -WindowStyle Hidden -PassThru
|
||||||
Wait-For { (Capture-Windows).Count -eq 1 } 'New process did not open selection.'
|
Wait-For { (Capture-Windows).Count -eq 1 } 'New process did not open selection.'
|
||||||
Send (Capture-Windows)[0] 0x100 27 # Esc cancels a selection only.
|
Send (Capture-Windows)[0] 0x100 27 # Esc cancels a selection only.
|
||||||
if (!$process.WaitForExit(5000) -or $process.ExitCode -ne 0) { throw 'Cancelling selection did not exit process.' }
|
if (!$process.WaitForExit(5000) -or $process.ExitCode -ne 0) { throw 'Cancelling selection did not exit process.' }
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Redirect HKCU and the shortcut destination; never replace the user's registration.
|
||||||
|
#include "../src/screenshot_notifications.h"
|
||||||
|
#include <shlobj.h>
|
||||||
|
#include <iostream>
|
||||||
|
static std::wstring programs;
|
||||||
|
static HRESULT testKnownFolder(REFKNOWNFOLDERID, DWORD, HANDLE, PWSTR *out)
|
||||||
|
{
|
||||||
|
*out=(PWSTR)CoTaskMemAlloc((programs.size()+1)*sizeof(wchar_t));
|
||||||
|
if(!*out) return E_OUTOFMEMORY;
|
||||||
|
wcscpy(*out,programs.c_str()); return S_OK;
|
||||||
|
}
|
||||||
|
#define SHGetKnownFolderPath testKnownFolder
|
||||||
|
#include "../src/screenshot_activation.cpp"
|
||||||
|
#undef SHGetKnownFolderPath
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
||||||
|
auto suffix=std::to_wstring(GetCurrentProcessId());
|
||||||
|
auto keyPath=L"Software\\KSwitcherRegistrationTest-"+suffix;
|
||||||
|
auto directory=std::filesystem::temp_directory_path()/(L"KSwitcherRegistrationTest-"+suffix);
|
||||||
|
std::filesystem::create_directories(directory); programs=directory.wstring();
|
||||||
|
HKEY isolated{}; int result=0;
|
||||||
|
try {
|
||||||
|
if(RegCreateKeyExW(HKEY_CURRENT_USER,keyPath.c_str(),0,nullptr,0,KEY_ALL_ACCESS,nullptr,&isolated,nullptr)!=ERROR_SUCCESS ||
|
||||||
|
RegOverridePredefKey(HKEY_CURRENT_USER,isolated)!=ERROR_SUCCESS) throw std::runtime_error("registry isolation failed");
|
||||||
|
ks::registerScreenshotNotifications();
|
||||||
|
wchar_t command[32768]{}; DWORD size=sizeof(command);
|
||||||
|
auto path=L"Software\\Classes\\CLSID\\{e3f78047-f0ad-49b6-9074-411931706167}\\LocalServer32";
|
||||||
|
if(RegGetValueW(HKEY_CURRENT_USER,path,nullptr,RRF_RT_REG_SZ,nullptr,command,&size)!=ERROR_SUCCESS)
|
||||||
|
throw std::runtime_error("COM server was not registered");
|
||||||
|
auto helper=std::filesystem::path(ks::executable()).parent_path()/L"KSwitcher.Screenshot.exe";
|
||||||
|
if(command!=L"\""+helper.wstring()+L"\" --toast-activated") throw std::runtime_error("COM server targets wrong executable");
|
||||||
|
Microsoft::WRL::ComPtr<IShellLinkW> link;
|
||||||
|
if(FAILED(CoCreateInstance(CLSID_ShellLink,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&link)))) throw std::runtime_error("read shortcut");
|
||||||
|
Microsoft::WRL::ComPtr<IPersistFile> file; link.As(&file);
|
||||||
|
if(FAILED(file->Load((directory/L"KSwitcher Screenshot.lnk").c_str(),STGM_READ))) throw std::runtime_error("shortcut missing");
|
||||||
|
wchar_t target[32768]{}; link->GetPath(target,32768,nullptr,SLGP_RAWPATH);
|
||||||
|
if(target!=helper.wstring()) throw std::runtime_error("shortcut targets wrong executable");
|
||||||
|
Microsoft::WRL::ComPtr<IPropertyStore> properties; link.As(&properties);
|
||||||
|
PROPVARIANT id{}; properties->GetValue(PKEY_AppUserModel_ID,&id);
|
||||||
|
bool valid=id.vt==VT_LPWSTR && std::wstring(id.pwszVal)==L"HrynCo.KSwitcher.Screenshot";
|
||||||
|
PropVariantClear(&id);
|
||||||
|
if(!valid) throw std::runtime_error("shortcut identity missing");
|
||||||
|
properties->GetValue(PKEY_AppUserModel_ToastActivatorCLSID,&id);
|
||||||
|
valid=id.vt==VT_CLSID && *id.puuid==ks::ActivatorId; PropVariantClear(&id);
|
||||||
|
if(!valid) throw std::runtime_error("shortcut activator missing");
|
||||||
|
ks::registerScreenshotNotifications(); // Registration is safe to repeat.
|
||||||
|
std::cout<<"Passed isolated screenshot registration checks\n";
|
||||||
|
} catch(const std::exception &e) { std::cerr<<e.what()<<'\n'; result=1; }
|
||||||
|
RegOverridePredefKey(HKEY_CURRENT_USER,nullptr);
|
||||||
|
if(isolated) RegCloseKey(isolated);
|
||||||
|
RegDeleteTreeW(HKEY_CURRENT_USER,keyPath.c_str());
|
||||||
|
std::filesystem::remove_all(directory);
|
||||||
|
CoUninitialize(); return result;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
|
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
||||||
try {
|
try {
|
||||||
INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES}; InitCommonControlsEx(&controls);
|
INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES}; InitCommonControlsEx(&controls);
|
||||||
ks::ScreenshotHost app{GetModuleHandleW(nullptr)};
|
ks::ScreenshotHost app{GetModuleHandleW(nullptr)};
|
||||||
@@ -162,7 +163,45 @@ int main()
|
|||||||
require(IsWindow(window),"Escape does not close the screenshot editor");
|
require(IsWindow(window),"Escape does not close the screenshot editor");
|
||||||
SendMessageW(window,WM_CLOSE,0,0);
|
SendMessageW(window,WM_CLOSE,0,0);
|
||||||
require(!IsWindow(window),"copied screenshot closes without confirmation");
|
require(!IsWindow(window),"copied screenshot closes without confirmation");
|
||||||
|
static bool published=false;
|
||||||
|
static bool notificationAccepted=false;
|
||||||
|
static HWND notificationWindow{};
|
||||||
|
static bool expectHidden=false;
|
||||||
|
app.notify=[](const ks::Clip &) {
|
||||||
|
if(!published) throw std::runtime_error("notification preceded clipboard publication");
|
||||||
|
if(expectHidden && IsWindowVisible(notificationWindow))
|
||||||
|
throw std::runtime_error("selection overlay is still visible when notification is sent");
|
||||||
|
return notificationAccepted;
|
||||||
|
};
|
||||||
|
for(bool autoOpen : {true,false}) for (bool accepted : {true, false}) {
|
||||||
|
app.autoOpenEditor=autoOpen;
|
||||||
|
published=false;
|
||||||
|
notificationAccepted=accepted;
|
||||||
|
ks::Capture selection(app);
|
||||||
|
selection.image=std::make_unique<ks::Surface>(100,100);
|
||||||
|
PatBlt(selection.image->dc,0,0,100,100,WHITENESS);
|
||||||
|
selection.start={10,20}; selection.end={70,60};
|
||||||
|
auto overlay=CreateWindowW(cls.lpszClassName,L"Selection test",WS_POPUP,0,0,100,100,app.window,nullptr,app.instance,&selection);
|
||||||
|
notificationWindow=overlay; expectHidden=!autoOpen;
|
||||||
|
ShowWindow(overlay,SW_SHOWNOACTIVATE);
|
||||||
|
ks::finishSelection(overlay,selection,[](HWND,const ks::Clip &clip)->DWORD {
|
||||||
|
auto pixels=ks::decodeImage(clip);
|
||||||
|
if(pixels.width!=60 || pixels.height!=40) throw std::runtime_error("automatic copy did not crop selection");
|
||||||
|
published=true; return 0;
|
||||||
|
});
|
||||||
|
if(!autoOpen && accepted) {
|
||||||
|
require(published && !IsWindow(overlay),"notification-first closes selection after successful submission");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
require(published && selection.copied && IsWindowVisible(overlay) && selection.editing,
|
||||||
|
"selection opens editor whether notification is accepted or rejected");
|
||||||
|
require(selection.image->width==60 && selection.image->height==40,
|
||||||
|
"automatically opened editor contains the selected region");
|
||||||
|
SendMessageW(overlay,WM_CLOSE,0,0);
|
||||||
|
require(!IsWindow(overlay),"automatically copied screenshot closes without confirmation");
|
||||||
|
}
|
||||||
DestroyWindow(app.window); app.window=nullptr;
|
DestroyWindow(app.window); app.window=nullptr;
|
||||||
|
CoUninitialize();
|
||||||
std::cout<<"Passed screenshot editor UI integration checks\n";
|
std::cout<<"Passed screenshot editor UI integration checks\n";
|
||||||
} catch(const std::exception &e) { std::cerr<<e.what()<<'\n'; return 1; }
|
} catch(const std::exception &e) { std::cerr<<e.what()<<'\n'; return 1; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -430,6 +430,13 @@ int main(int argc, char **argv)
|
|||||||
auto invalid = settings;
|
auto invalid = settings;
|
||||||
auto captureSettings = settings;
|
auto captureSettings = settings;
|
||||||
captureSettings.screenshot = L"Ctrl+Shift+F9";
|
captureSettings.screenshot = L"Ctrl+Shift+F9";
|
||||||
|
require(!captureSettings.screenshotAutoOpenEditor,"notification-first capture is the default");
|
||||||
|
captureSettings.screenshotAutoOpenEditor=true;
|
||||||
|
require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor,
|
||||||
|
"explicit automatic editor preference survives settings roundtrip");
|
||||||
|
captureSettings.screenshotAutoOpenEditor=false;
|
||||||
|
require(!ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor,
|
||||||
|
"notification-first preference survives settings roundtrip");
|
||||||
require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshot == captureSettings.screenshot,
|
require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshot == captureSettings.screenshot,
|
||||||
"screen capture shortcut roundtrip");
|
"screen capture shortcut roundtrip");
|
||||||
captureSettings.screenshot = captureSettings.clipboard;
|
captureSettings.screenshot = captureSettings.clipboard;
|
||||||
|
|||||||
Reference in New Issue
Block a user