Release v0.6.20: edit history images and share smooth wheel handling
This commit is contained in:
+6
-2
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.19 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.20 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()
|
||||||
@@ -17,6 +17,8 @@ set_target_properties(KSwitcherConfig PROPERTIES OUTPUT_NAME "KSwitcher.Config")
|
|||||||
target_compile_definitions(KSwitcherConfig PRIVATE KS_CONFIG_APP)
|
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)
|
||||||
|
add_executable(KSwitcherScrollTests tests/scroll.cpp)
|
||||||
|
target_link_libraries(KSwitcherScrollTests PRIVATE kscore)
|
||||||
target_link_libraries(KSwitcherTests PRIVATE kscore)
|
target_link_libraries(KSwitcherTests PRIVATE kscore)
|
||||||
add_executable(KSwitcherIntegration tests/integration.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/app.cpp src/screenshot_notifications.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_notifications.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)
|
||||||
@@ -39,7 +41,7 @@ endif()
|
|||||||
add_executable(KSwitcherOcrTests tests/ocr.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/app.cpp src/screenshot_notifications.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcherOcrTests tests/ocr.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/app.cpp src/screenshot_notifications.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
target_link_libraries(KSwitcherOcrTests PRIVATE kscore)
|
target_link_libraries(KSwitcherOcrTests PRIVATE kscore)
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
set_property(TARGET kscore KSwitcher KSwitcherConfig KSwitcherScreenshot KSwitcherTests KSwitcherIntegration KSwitcherInputTests KSwitcherOcrTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
set_property(TARGET kscore KSwitcher KSwitcherConfig KSwitcherScreenshot KSwitcherTests KSwitcherScrollTests KSwitcherIntegration KSwitcherInputTests KSwitcherOcrTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||||
target_compile_options(kscore PRIVATE /W4 /utf-8)
|
target_compile_options(kscore PRIVATE /W4 /utf-8)
|
||||||
target_compile_options(KSwitcher PRIVATE /W4 /utf-8)
|
target_compile_options(KSwitcher PRIVATE /W4 /utf-8)
|
||||||
target_compile_options(KSwitcherConfig PRIVATE /W4 /utf-8)
|
target_compile_options(KSwitcherConfig PRIVATE /W4 /utf-8)
|
||||||
@@ -49,12 +51,14 @@ else()
|
|||||||
target_link_options(KSwitcherConfig PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherConfig PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||||
target_link_options(KSwitcherScreenshot PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherScreenshot PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||||
target_link_options(KSwitcherTests PRIVATE -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherTests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
|
target_link_options(KSwitcherScrollTests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
target_link_options(KSwitcherIntegration PRIVATE -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherIntegration PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
target_link_options(KSwitcherInputTests PRIVATE -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherInputTests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
target_link_options(KSwitcherOcrTests PRIVATE -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherOcrTests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
endif()
|
endif()
|
||||||
enable_testing()
|
enable_testing()
|
||||||
add_test(NAME native-regressions COMMAND KSwitcherTests)
|
add_test(NAME native-regressions COMMAND KSwitcherTests)
|
||||||
|
add_test(NAME scroll-regressions COMMAND KSwitcherScrollTests)
|
||||||
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
|
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
|
||||||
add_test(NAME screenshot-ui-regressions COMMAND KSwitcherScreenshotUITests)
|
add_test(NAME screenshot-ui-regressions COMMAND KSwitcherScreenshotUITests)
|
||||||
add_test(NAME updater-regressions COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/update.ps1" -Executable "$<TARGET_FILE:KSwitcher>")
|
add_test(NAME updater-regressions COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/update.ps1" -Executable "$<TARGET_FILE:KSwitcher>")
|
||||||
|
|||||||
@@ -278,6 +278,21 @@ existing saved shortcuts are preserved. While KSwitcher runs, this binding opens
|
|||||||
|
|
||||||
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.
|
||||||
|
For an image, **Edit image**, **Ctrl+O**, or **Edit image in screenshot editor**
|
||||||
|
in the context menu opens the original image in the built-in screenshot editor.
|
||||||
|
Annotate it there, then copy the result. Opening the editor leaves the
|
||||||
|
clipboard and original history entry unchanged. Text items still open as text;
|
||||||
|
Enter and double-click still paste the selected item.
|
||||||
|
|
||||||
|
Local builds made with `build.ps1` show **[LOCAL DEV]** in window titles and
|
||||||
|
the tray tooltip. The marker is enabled by `.local-dev` beside the executable;
|
||||||
|
release packages do not include that marker.
|
||||||
|
|
||||||
|
Fast/inertial wheel events use a shared batching handler in history, layout and
|
||||||
|
snippet lists, the log table, multiline text/details fields, combo boxes and
|
||||||
|
scrollable settings windows. High-resolution partial wheel steps are retained.
|
||||||
|
History pauses new preview loads while scrolling and keeps visible thumbnails
|
||||||
|
cached, preventing continuous flicker and needless redraws.
|
||||||
Pressing the history shortcut a second time pastes the second-most-recent item.
|
Pressing the history shortcut a second time pastes the second-most-recent item.
|
||||||
|
|
||||||
## Data and migration
|
## Data and migration
|
||||||
|
|||||||
Binary file not shown.
+10
-9
@@ -1,23 +1,24 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.19 — 2026-10-31
|
## v0.6.20 — 2026-10-31
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Retry a silently dropped Ctrl+C up to three total attempts, only while the transaction clipboard remains unchanged and the original input context is still valid. Failed snippet verification preserves the trigger and following input without sending Enter.
|
- Open clipboard-history images in the built-in screenshot editor with **Edit image**, **Ctrl+O**, or **Edit image in screenshot editor** in the context menu. PNG, DIB and DIBV5 open at full resolution through an encrypted local cache, without requiring notifications or changing the clipboard/original history entry. Copy the annotated result explicitly from the editor.
|
||||||
- Exclude temporary transaction clipboard contents from Windows clipboard history and cloud synchronization. The original clipboard snapshot is still restored after the operation when ownership permits.
|
- Fix continuous history flicker by keeping visible previews cached, invalidating only completed preview rows and preserving the top entry across background refreshes.
|
||||||
- Fix snippet integration test isolation: restore a multiline editor after earlier single-line scenarios, reset layout/correction state, coordinate test-editor clipboard access and disable unrelated background update checks.
|
- Handle fast/inertial wheels through one shared batching mechanism for history, layout/snippet lists, the log table, multiline text/details fields, combo boxes and scrollable settings windows. Preserve partial wheel deltas, clamp list boundaries and pause new history previews until scrolling stops.
|
||||||
- Add deterministic coverage for one dropped copy and exhausted retries. Keep exact assertions for Enter ordering, buffered typing, clipboard restoration and subsequent auto-correction.
|
- Mark local developer builds with **[LOCAL DEV]** in window titles and the tray tooltip. Release packages omit the development marker. This does not create a separate data profile.
|
||||||
|
- Right-clicking a history entry selects that entry before showing its actions. Enter and double-click continue to paste; text items still open in the full-text viewer.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
The fixes passed 230 native regression checks, 472 input/UI checks, screenshot editor UI and notification registration checks, 48 updater checks and updater progress checks. The full editor integration suite passed all 63 assertions; conversion-specific diagnostics also passed. The final snippet suite passed 20 consecutive runs, including injected copy failures and the three-attempt limit. The v0.6.19 staged Release build also passed the native, input/UI, screenshot and updater checks; the installed application was not replaced or restarted.
|
Passed 406 native regressions, 473 input/UI checks, shared wheel batching tests, screenshot editor UI, notification registration, 48 updater checks and updater progress checks. Manual testing confirmed image editing from history and resolved clipboard flicker and Logitech inertial-wheel beeping. Shared wheel tests cover lists, edits, tables, combo boxes and windows, partial steps, large bursts, selection preservation and list boundaries.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
The fix does not retry paste blindly: an editor may already have changed its selection or text. Clipboard contention, editor capabilities and input privileges can still prevent an operation. Existing accessibility readback limitations and problem-only diagnostic logging remain unchanged.
|
Image decoding or a normalized image larger than the 128 MiB editor-transfer limit can prevent opening; an error is shown. Existing clipboard, OCR and editor privilege limitations remain. The wheel over screenshot annotations changes size/thickness rather than scrolling.
|
||||||
|
|
||||||
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||||
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
For manual installation, exit KSwitcher, close screenshot editors 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 all three executables, a release manifest, offline dictionaries, documentation and license notices. It contains no personal settings, clipboard history, Tesseract installation or language models.
|
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains all three executables, a release manifest, offline dictionaries, documentation and license notices. It contains no personal settings, clipboard history, development marker, Tesseract installation or language models.
|
||||||
|
|
||||||
Build and package:
|
Build and package:
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
8b2008e97ca625b7eebdf509c038d6d1f6c5c853dce6da1b939abaa18223e26d KSwitcher-win-x64.zip
|
0b3c4fd1346d0b4eeb73d276193c5d2789fd32986c3cc75384cf489f23629678 KSwitcher-win-x64.zip
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ $outDir = Join-Path $root 'build'
|
|||||||
New-Item -ItemType Directory -Force $outDir | Out-Null
|
New-Item -ItemType Directory -Force $outDir | Out-Null
|
||||||
$executablePath = if ($Stage) { Join-Path $outDir 'staged\KSwitcher.exe' } else { Join-Path $outDir 'KSwitcher.exe' }
|
$executablePath = if ($Stage) { Join-Path $outDir 'staged\KSwitcher.exe' } else { Join-Path $outDir 'KSwitcher.exe' }
|
||||||
New-Item -ItemType Directory -Force (Split-Path $executablePath) | Out-Null
|
New-Item -ItemType Directory -Force (Split-Path $executablePath) | Out-Null
|
||||||
|
Set-Content -LiteralPath (Join-Path $outDir '.local-dev') -Value 'Local development build' -Encoding ascii
|
||||||
|
Set-Content -LiteralPath (Join-Path (Split-Path $executablePath) '.local-dev') -Value 'Local development build' -Encoding ascii
|
||||||
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')
|
||||||
@@ -39,6 +41,10 @@ try {
|
|||||||
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) {
|
||||||
|
& $compiler @common tests/scroll.cpp @libs -static -s -o build/KSwitcherScrollTests.exe
|
||||||
|
if ($LASTEXITCODE) { throw 'Scroll test build failed' }
|
||||||
|
& .\build\KSwitcherScrollTests.exe
|
||||||
|
if ($LASTEXITCODE) { throw 'Scroll tests failed' }
|
||||||
& $compiler @common tests/tests.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 @libs -static -s -o build/KSwitcherTests.exe
|
& $compiler @common tests/tests.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 @libs -static -s -o build/KSwitcherTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Test build failed' }
|
if ($LASTEXITCODE) { throw 'Test build failed' }
|
||||||
& .\build\KSwitcherTests.exe
|
& .\build\KSwitcherTests.exe
|
||||||
|
|||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,6,19,0
|
FILEVERSION 0,6,20,0
|
||||||
PRODUCTVERSION 0,6,19,0
|
PRODUCTVERSION 0,6,20,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.6.19"
|
VALUE "FileVersion", "0.6.20"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+1
-1
@@ -433,7 +433,7 @@ void App::addTray()
|
|||||||
icon.uCallbackMessage = TrayMessage;
|
icon.uCallbackMessage = TrayMessage;
|
||||||
icon.hIcon = (HICON)LoadImageW(instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
|
icon.hIcon = (HICON)LoadImageW(instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
|
||||||
GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_SHARED);
|
GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_SHARED);
|
||||||
wcscpy_s(icon.szTip, L"KSwitcher — keyboard layouts & clipboard");
|
wcscpy_s(icon.szTip, localBuildTitle(L"KSwitcher — keyboard layouts & clipboard").c_str());
|
||||||
Shell_NotifyIconW(NIM_ADD, &icon);
|
Shell_NotifyIconW(NIM_ADD, &icon);
|
||||||
icon.uVersion = NOTIFYICON_VERSION_4;
|
icon.uVersion = NOTIFYICON_VERSION_4;
|
||||||
Shell_NotifyIconW(NIM_SETVERSION, &icon);
|
Shell_NotifyIconW(NIM_SETVERSION, &icon);
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class App
|
|||||||
};
|
};
|
||||||
void showHistory(App &, bool visible = true);
|
void showHistory(App &, bool visible = true);
|
||||||
void launchScreenshot(App &);
|
void launchScreenshot(App &);
|
||||||
|
void launchScreenshotEditor(App &, const Clip &);
|
||||||
void refreshHistory(App &);
|
void refreshHistory(App &);
|
||||||
void showSettings(App &, bool visible = true);
|
void showSettings(App &, bool visible = true);
|
||||||
void showLogs(App &);
|
void showLogs(App &);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ks {
|
||||||
|
// Keep the visible working set even in unusually tall windows. Never evict a
|
||||||
|
// visible preview to admit another visible preview: that creates a paint loop.
|
||||||
|
template<class T>
|
||||||
|
void trimHistoryPreviews(std::map<std::wstring,T> &cache,
|
||||||
|
const std::set<std::wstring> &visible, size_t limit=32)
|
||||||
|
{
|
||||||
|
for(auto it=cache.begin(); cache.size()>limit && it!=cache.end();)
|
||||||
|
if(!visible.contains(it->first)) it=cache.erase(it);
|
||||||
|
else ++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,16 @@
|
|||||||
|
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
|
inline std::wstring localBuildTitle(const std::wstring &title)
|
||||||
|
{
|
||||||
|
static const bool development=[] {
|
||||||
|
wchar_t path[32768]{};
|
||||||
|
if(!GetModuleFileNameW(nullptr,path,std::size(path))) return false;
|
||||||
|
std::error_code error;
|
||||||
|
return std::filesystem::exists(std::filesystem::path(path).parent_path()/L".local-dev",error);
|
||||||
|
}();
|
||||||
|
return development?title+L" [LOCAL DEV]":title;
|
||||||
|
}
|
||||||
using Bytes = std::vector<unsigned char>;
|
using Bytes = std::vector<unsigned char>;
|
||||||
struct Format
|
struct Format
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -841,7 +841,7 @@ void edit(HWND w, Capture &c)
|
|||||||
SetWindowLongPtrW(w,GWL_EXSTYLE,(GetWindowLongPtrW(w,GWL_EXSTYLE)|WS_EX_APPWINDOW)&~WS_EX_TOOLWINDOW);
|
SetWindowLongPtrW(w,GWL_EXSTYLE,(GetWindowLongPtrW(w,GWL_EXSTYLE)|WS_EX_APPWINDOW)&~WS_EX_TOOLWINDOW);
|
||||||
SetWindowLongPtrW(w,GWL_STYLE,WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN);
|
SetWindowLongPtrW(w,GWL_STYLE,WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN);
|
||||||
auto title=L"KSwitcher — Screenshot #"+std::to_wstring(nextEditorIndex++);
|
auto title=L"KSwitcher — Screenshot #"+std::to_wstring(nextEditorIndex++);
|
||||||
SetWindowTextW(w,title.c_str());
|
SetWindowTextW(w,localBuildTitle(title).c_str());
|
||||||
INITCOMMONCONTROLSEX statusControls{sizeof(statusControls),ICC_BAR_CLASSES};
|
INITCOMMONCONTROLSEX statusControls{sizeof(statusControls),ICC_BAR_CLASSES};
|
||||||
InitCommonControlsEx(&statusControls);
|
InitCommonControlsEx(&statusControls);
|
||||||
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
|
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "screenshot_notifications.h"
|
||||||
|
|
||||||
namespace ks {
|
namespace ks {
|
||||||
void launchScreenshot(App &app)
|
static void launch(App &app, const Clip *clip)
|
||||||
{
|
{
|
||||||
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-open-editor="+
|
auto command=L"\""+path.wstring()+L"\" ";
|
||||||
(app.settings.screenshotAutoOpenEditor?L"1":L"0");
|
if(clip) command+=L"--open kswitcher-screenshot:"+cacheScreenshotForEditor(*clip);
|
||||||
|
else command+=std::wstring(L"--auto-open-editor=")+(app.settings.screenshotAutoOpenEditor?L"1":L"0");
|
||||||
if(app.settings.writeLog) command+=L" --write-log";
|
if(app.settings.writeLog) command+=L" --write-log";
|
||||||
STARTUPINFOW startup{sizeof(startup)};
|
STARTUPINFOW startup{sizeof(startup)};
|
||||||
PROCESS_INFORMATION process{};
|
PROCESS_INFORMATION process{};
|
||||||
@@ -20,4 +22,6 @@ void launchScreenshot(App &app)
|
|||||||
CloseHandle(process.hThread);
|
CloseHandle(process.hThread);
|
||||||
CloseHandle(process.hProcess);
|
CloseHandle(process.hProcess);
|
||||||
}
|
}
|
||||||
|
void launchScreenshot(App &app) { launch(app,nullptr); }
|
||||||
|
void launchScreenshotEditor(App &app,const Clip &clip) { launch(app,&clip); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,19 @@ std::wstring escapeXml(const std::wstring &s)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
std::wstring cacheScreenshotForEditor(const Clip &clip)
|
||||||
|
{
|
||||||
|
// Normalize PNG/DIBV5 and other supported clipboard images at full resolution.
|
||||||
|
auto pixels=decodeImage(clip);
|
||||||
|
if(pixels.bgra.empty() || pixels.bgra.size()>128*1024*1024-sizeof(BITMAPINFOHEADER))
|
||||||
|
throw std::runtime_error("Image is too large for the screenshot editor");
|
||||||
|
auto dib=imageDib(pixels);
|
||||||
|
auto directory=cacheDirectory(); cleanCache(directory);
|
||||||
|
GUID guid{}; check(CoCreateGuid(&guid)); wchar_t value[40]{}; StringFromGUID2(guid,value,40);
|
||||||
|
std::wstring id(value+1,36);
|
||||||
|
atomicWrite(directory/(id+L".capture"),protect(dib.data));
|
||||||
|
return id;
|
||||||
|
}
|
||||||
Clip loadNotifiedScreenshot(const std::wstring &id)
|
Clip loadNotifiedScreenshot(const std::wstring &id)
|
||||||
{
|
{
|
||||||
if(!validScreenshotToken(id)) throw std::runtime_error("Invalid screenshot notification");
|
if(!validScreenshotToken(id)) throw std::runtime_error("Invalid screenshot notification");
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ int runScreenshotToastActivator();
|
|||||||
DWORD startScreenshotActivationListener();
|
DWORD startScreenshotActivationListener();
|
||||||
bool validScreenshotToken(const std::wstring &);
|
bool validScreenshotToken(const std::wstring &);
|
||||||
Clip loadNotifiedScreenshot(const std::wstring &);
|
Clip loadNotifiedScreenshot(const std::wstring &);
|
||||||
|
std::wstring cacheScreenshotForEditor(const Clip &);
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-11
@@ -1,5 +1,7 @@
|
|||||||
#include "../resources/resource.h"
|
#include "../resources/resource.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "history_preview.h"
|
||||||
|
#include "ui_scroll.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "ocr.h"
|
#include "ocr.h"
|
||||||
#include "richtext.h"
|
#include "richtext.h"
|
||||||
@@ -238,6 +240,9 @@ HWND control(HWND parent, const wchar_t *cls, const wchar_t *title, DWORD style,
|
|||||||
scale(parent, width), scale(parent, height), parent, (HMENU)(INT_PTR)id,
|
scale(parent, width), scale(parent, height), parent, (HMENU)(INT_PTR)id,
|
||||||
GetModuleHandleW(nullptr), nullptr);
|
GetModuleHandleW(nullptr), nullptr);
|
||||||
SendMessageW(w, WM_SETFONT, (WPARAM)GetPropW(parent, L"Font"), TRUE);
|
SendMessageW(w, WM_SETFONT, (WPARAM)GetPropW(parent, L"Font"), TRUE);
|
||||||
|
if(wcscmp(cls,L"LISTBOX")==0 || wcscmp(cls,WC_LISTVIEWW)==0 ||
|
||||||
|
wcscmp(cls,L"COMBOBOX")==0 || (wcscmp(cls,L"EDIT")==0 && (style&ES_MULTILINE)))
|
||||||
|
enableWheelBatching(w,wcscmp(cls,L"LISTBOX")==0);
|
||||||
if (auto *layout = windowLayoutState(parent); layout && layout->scrolling)
|
if (auto *layout = windowLayoutState(parent); layout && layout->scrolling)
|
||||||
{
|
{
|
||||||
layout->controls.push_back({w, {x, y, x + width, y + height}});
|
layout->controls.push_back({w, {x, y, x + width, y + height}});
|
||||||
@@ -368,11 +373,12 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
|
|||||||
int top = monitor.rcWork.top + std::max(0L, (monitor.rcWork.bottom - monitor.rcWork.top - outerHeight) / 2);
|
int top = monitor.rcWork.top + std::max(0L, (monitor.rcWork.bottom - monitor.rcWork.top - outerHeight) / 2);
|
||||||
SetPropW(w, L"WindowLayout", new WindowLayout{width, height, 0, 0, !(extra & WS_THICKFRAME)});
|
SetPropW(w, L"WindowLayout", new WindowLayout{width, height, 0, 0, !(extra & WS_THICKFRAME)});
|
||||||
SetWindowSubclass(w, layoutProc, 2, 0);
|
SetWindowSubclass(w, layoutProc, 2, 0);
|
||||||
|
if(windowLayoutState(w)->scrolling) enableWheelBatching(w);
|
||||||
RECT initial = fitWindowRect({left, top, left + outerWidth, top + outerHeight}, monitor.rcWork);
|
RECT initial = fitWindowRect({left, top, left + outerWidth, top + outerHeight}, monitor.rcWork);
|
||||||
SetWindowPos(w, nullptr, initial.left, initial.top, initial.right - initial.left, initial.bottom - initial.top,
|
SetWindowPos(w, nullptr, initial.left, initial.top, initial.right - initial.left, initial.bottom - initial.top,
|
||||||
SWP_NOACTIVATE | SWP_NOZORDER);
|
SWP_NOACTIVATE | SWP_NOZORDER);
|
||||||
arrangeWindow(w);
|
arrangeWindow(w);
|
||||||
SetWindowTextW(w, title);
|
SetWindowTextW(w, localBuildTitle(title).c_str());
|
||||||
auto smallIcon = (HICON)LoadImageW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
|
auto smallIcon = (HICON)LoadImageW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
|
||||||
GetSystemMetricsForDpi(SM_CXSMICON, dpi),
|
GetSystemMetricsForDpi(SM_CXSMICON, dpi),
|
||||||
GetSystemMetricsForDpi(SM_CYSMICON, dpi), LR_SHARED);
|
GetSystemMetricsForDpi(SM_CYSMICON, dpi), LR_SHARED);
|
||||||
@@ -478,7 +484,17 @@ struct HistoryView
|
|||||||
std::map<std::wstring, std::string> richTexts;
|
std::map<std::wstring, std::string> richTexts;
|
||||||
HWND rich{};
|
HWND rich{};
|
||||||
std::set<std::wstring> pending;
|
std::set<std::wstring> pending;
|
||||||
|
bool scrolling{};
|
||||||
};
|
};
|
||||||
|
bool selectedImage(HWND w, HistoryView *v)
|
||||||
|
{
|
||||||
|
int i=(int)SendDlgItemMessageW(w,102,LB_GETCURSEL,0,0);
|
||||||
|
return i>=0 && i<(int)v->items.size() && v->items[i].image;
|
||||||
|
}
|
||||||
|
void updateHistoryActions(HWND w, HistoryView *v)
|
||||||
|
{
|
||||||
|
SetDlgItemTextW(w,114,selectedImage(w,v)?L"Edit image":L"Open item");
|
||||||
|
}
|
||||||
std::wstring selected(HWND w, HistoryView *v)
|
std::wstring selected(HWND w, HistoryView *v)
|
||||||
{
|
{
|
||||||
int i = (int)SendDlgItemMessageW(w, 102, LB_GETCURSEL, 0, 0);
|
int i = (int)SendDlgItemMessageW(w, 102, LB_GETCURSEL, 0, 0);
|
||||||
@@ -507,6 +523,7 @@ void search(HWND w, HistoryView *v)
|
|||||||
}
|
}
|
||||||
void requestPreview(HWND w, HistoryView *v, const Entry &entry)
|
void requestPreview(HWND w, HistoryView *v, const Entry &entry)
|
||||||
{
|
{
|
||||||
|
if(v->scrolling) return;
|
||||||
const auto id = entry.id;
|
const auto id = entry.id;
|
||||||
const bool image = entry.image;
|
const bool image = entry.image;
|
||||||
if (v->thumbnails.contains(id) || v->richTexts.contains(id) || !v->pending.insert(id).second)
|
if (v->thumbnails.contains(id) || v->richTexts.contains(id) || !v->pending.insert(id).second)
|
||||||
@@ -690,6 +707,12 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
{
|
{
|
||||||
switch (msg)
|
switch (msg)
|
||||||
{
|
{
|
||||||
|
case ScrollActivity:
|
||||||
|
if((HWND)wp==GetDlgItem(w,102)) {
|
||||||
|
v->scrolling=lp!=0;
|
||||||
|
if(!v->scrolling) InvalidateRect((HWND)wp,nullptr,FALSE);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
case WM_COMMAND:
|
case WM_COMMAND:
|
||||||
{
|
{
|
||||||
int id = LOWORD(wp);
|
int id = LOWORD(wp);
|
||||||
@@ -702,6 +725,7 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
if (id == 102 && HIWORD(wp) == LBN_SELCHANGE)
|
if (id == 102 && HIWORD(wp) == LBN_SELCHANGE)
|
||||||
{
|
{
|
||||||
|
updateHistoryActions(w,v);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id == 102 && HIWORD(wp) == LBN_DBLCLK)
|
if (id == 102 && HIWORD(wp) == LBN_DBLCLK)
|
||||||
@@ -747,10 +771,13 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if (result->generation != v->generation)
|
if (result->generation != v->generation)
|
||||||
return 0;
|
return 0;
|
||||||
auto old = selected(w, v);
|
auto old = selected(w, v);
|
||||||
|
int top=(int)SendDlgItemMessageW(w,102,LB_GETTOPINDEX,0,0);
|
||||||
|
auto topId=top>=0 && top<(int)v->items.size()?v->items[top].id:L"";
|
||||||
v->items = std::move(result->items);
|
v->items = std::move(result->items);
|
||||||
SendDlgItemMessageW(w, 102, WM_SETREDRAW, FALSE, 0);
|
SendDlgItemMessageW(w, 102, WM_SETREDRAW, FALSE, 0);
|
||||||
SendDlgItemMessageW(w, 102, LB_RESETCONTENT, 0, 0);
|
SendDlgItemMessageW(w, 102, LB_RESETCONTENT, 0, 0);
|
||||||
int select = 0;
|
int select = 0;
|
||||||
|
int restoreTop=0;
|
||||||
for (size_t i = 0; i < v->items.size(); ++i)
|
for (size_t i = 0; i < v->items.size(); ++i)
|
||||||
{
|
{
|
||||||
auto &e = v->items[i];
|
auto &e = v->items[i];
|
||||||
@@ -758,8 +785,11 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
SendDlgItemMessageW(w, 102, LB_ADDSTRING, 0, (LPARAM)text.c_str());
|
SendDlgItemMessageW(w, 102, LB_ADDSTRING, 0, (LPARAM)text.c_str());
|
||||||
if (e.id == old)
|
if (e.id == old)
|
||||||
select = (int)i;
|
select = (int)i;
|
||||||
|
if(e.id==topId) restoreTop=(int)i;
|
||||||
}
|
}
|
||||||
SendDlgItemMessageW(w, 102, LB_SETCURSEL, select, 0);
|
SendDlgItemMessageW(w, 102, LB_SETCURSEL, select, 0);
|
||||||
|
SendDlgItemMessageW(w,102,LB_SETTOPINDEX,restoreTop,0);
|
||||||
|
updateHistoryActions(w,v);
|
||||||
SendDlgItemMessageW(w, 102, WM_SETREDRAW, TRUE, 0);
|
SendDlgItemMessageW(w, 102, WM_SETREDRAW, TRUE, 0);
|
||||||
InvalidateRect(GetDlgItem(w, 102), nullptr, TRUE);
|
InvalidateRect(GetDlgItem(w, 102), nullptr, TRUE);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -770,16 +800,25 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if (result->generation != v->generation || !IsWindowVisible(w))
|
if (result->generation != v->generation || !IsWindowVisible(w))
|
||||||
return 0;
|
return 0;
|
||||||
v->pending.erase(result->id);
|
v->pending.erase(result->id);
|
||||||
// At most ~3.3 MiB, irrespective of history length or source image size.
|
auto list=GetDlgItem(w,102);
|
||||||
if (v->thumbnails.size() >= 32)
|
RECT client{}; GetClientRect(list,&client);
|
||||||
v->thumbnails.erase(v->thumbnails.begin());
|
std::set<std::wstring> visible;
|
||||||
if (v->richTexts.size() >= 32)
|
RECT changed{}; bool repaint=false;
|
||||||
v->richTexts.erase(v->richTexts.begin());
|
int top=(int)SendMessageW(list,LB_GETTOPINDEX,0,0);
|
||||||
|
for(int i=std::max(0,top);i<(int)v->items.size();++i) {
|
||||||
|
RECT row{}, intersection{};
|
||||||
|
if(SendMessageW(list,LB_GETITEMRECT,i,(LPARAM)&row)==LB_ERR || row.top>=client.bottom) break;
|
||||||
|
if(!IntersectRect(&intersection,&row,&client)) continue;
|
||||||
|
visible.insert(v->items[i].id);
|
||||||
|
if(v->items[i].id==result->id) { changed=intersection; repaint=true; }
|
||||||
|
}
|
||||||
if (!result->rtf.empty())
|
if (!result->rtf.empty())
|
||||||
v->richTexts[result->id] = std::move(result->rtf);
|
v->richTexts[result->id] = std::move(result->rtf);
|
||||||
else
|
else
|
||||||
v->thumbnails[result->id] = std::move(result->pixels);
|
v->thumbnails[result->id] = std::move(result->pixels);
|
||||||
InvalidateRect(GetDlgItem(w, 102), nullptr, FALSE);
|
trimHistoryPreviews(v->thumbnails,visible);
|
||||||
|
trimHistoryPreviews(v->richTexts,visible);
|
||||||
|
if(repaint) InvalidateRect(list,&changed,FALSE);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_MEASUREITEM:
|
case WM_MEASUREITEM:
|
||||||
@@ -798,15 +837,29 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
break;
|
break;
|
||||||
case WM_CONTEXTMENU:
|
case WM_CONTEXTMENU:
|
||||||
{
|
{
|
||||||
|
auto list=GetDlgItem(w,102);
|
||||||
|
if((HWND)wp!=list) break;
|
||||||
|
POINT p{(short)LOWORD(lp),(short)HIWORD(lp)};
|
||||||
|
if(lp!=-1) {
|
||||||
|
POINT hit=p; ScreenToClient(list,&hit);
|
||||||
|
auto item=SendMessageW(list,LB_ITEMFROMPOINT,0,MAKELPARAM(hit.x,hit.y));
|
||||||
|
if(HIWORD(item)) return 0;
|
||||||
|
SendMessageW(list,LB_SETCURSEL,LOWORD(item),0);
|
||||||
|
updateHistoryActions(w,v);
|
||||||
|
} else {
|
||||||
|
RECT row{};
|
||||||
|
auto index=SendMessageW(list,LB_GETCURSEL,0,0);
|
||||||
|
if(index==LB_ERR) return 0;
|
||||||
|
SendMessageW(list,LB_GETITEMRECT,index,(LPARAM)&row);
|
||||||
|
p={row.left,row.top}; ClientToScreen(list,&p);
|
||||||
|
}
|
||||||
HMENU menu = CreatePopupMenu();
|
HMENU menu = CreatePopupMenu();
|
||||||
AppendMenuW(menu, MF_STRING, 110, L"Paste");
|
AppendMenuW(menu, MF_STRING, 110, L"Paste");
|
||||||
AppendMenuW(menu, MF_STRING, 111, L"Paste plain text");
|
AppendMenuW(menu, MF_STRING, 111, L"Paste plain text");
|
||||||
AppendMenuW(menu, MF_STRING, 112, L"Pin / unpin");
|
AppendMenuW(menu, MF_STRING, 112, L"Pin / unpin");
|
||||||
AppendMenuW(menu, MF_STRING, 113, L"Delete");
|
AppendMenuW(menu, MF_STRING, 113, L"Delete");
|
||||||
AppendMenuW(menu, MF_STRING, 114, L"Open full item");
|
AppendMenuW(menu, MF_STRING, 114, selectedImage(w,v)?L"Edit image in screenshot editor":L"Open full item");
|
||||||
AppendMenuW(menu, MF_STRING, 115, L"Copy text from image (OCR)");
|
AppendMenuW(menu, MF_STRING, 115, L"Copy text from image (OCR)");
|
||||||
POINT p;
|
|
||||||
GetCursorPos(&p);
|
|
||||||
int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, p.x, p.y, 0, w, nullptr);
|
int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, p.x, p.y, 0, w, nullptr);
|
||||||
DestroyMenu(menu);
|
DestroyMenu(menu);
|
||||||
if (cmd)
|
if (cmd)
|
||||||
@@ -1700,11 +1753,17 @@ void showEntry(App &app, const std::wstring &id)
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
auto clip = app.history->load(id);
|
auto clip = app.history->load(id);
|
||||||
|
if(std::any_of(clip.formats.begin(),clip.formats.end(),[](const auto &format) {
|
||||||
|
return format.id==CF_DIB || format.id==CF_DIBV5 || format.name==L"PNG";
|
||||||
|
})) {
|
||||||
|
launchScreenshotEditor(app,clip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto text = clip.text();
|
auto text = clip.text();
|
||||||
if (text.empty())
|
if (text.empty())
|
||||||
{
|
{
|
||||||
MessageBoxW(app.historyWindow,
|
MessageBoxW(app.historyWindow,
|
||||||
L"This item contains an image or files. Use the preview or paste it into the "
|
L"This item contains files. Paste it into the "
|
||||||
L"destination application.",
|
L"destination application.",
|
||||||
L"Clipboard item", MB_OK);
|
L"Clipboard item", MB_OK);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "platform.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <commctrl.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
namespace ks {
|
||||||
|
inline constexpr UINT ScrollActivity=WM_APP+42;
|
||||||
|
inline constexpr UINT_PTR ScrollFrameTimer=0x4b5301, ScrollIdleTimer=0x4b5302;
|
||||||
|
struct WheelBatch {
|
||||||
|
long long vertical{}, horizontal{};
|
||||||
|
WORD keys{};
|
||||||
|
LPARAM point{};
|
||||||
|
bool frame{}, active{}, listBox{};
|
||||||
|
int remainder{};
|
||||||
|
};
|
||||||
|
inline LRESULT CALLBACK wheelBatchProc(HWND w,UINT msg,WPARAM wp,LPARAM lp,UINT_PTR id,DWORD_PTR data)
|
||||||
|
{
|
||||||
|
auto *batch=reinterpret_cast<WheelBatch *>(data);
|
||||||
|
if(msg==WM_MOUSEWHEEL || msg==WM_MOUSEHWHEEL) {
|
||||||
|
(msg==WM_MOUSEWHEEL?batch->vertical:batch->horizontal)+=GET_WHEEL_DELTA_WPARAM(wp);
|
||||||
|
batch->keys=GET_KEYSTATE_WPARAM(wp); batch->point=lp;
|
||||||
|
if(!batch->active) {
|
||||||
|
batch->active=true;
|
||||||
|
SendMessageW(GetParent(w),ScrollActivity,(WPARAM)w,TRUE);
|
||||||
|
}
|
||||||
|
// Do not restart the frame timer: an uninterrupted wheel stream must
|
||||||
|
// keep moving, rather than wait for a debounce interval to expire.
|
||||||
|
if(!batch->frame) batch->frame=SetTimer(w,ScrollFrameTimer,16,nullptr)!=0;
|
||||||
|
SetTimer(w,ScrollIdleTimer,100,nullptr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if(msg==WM_TIMER && wp==ScrollFrameTimer) {
|
||||||
|
KillTimer(w,ScrollFrameTimer); batch->frame=false;
|
||||||
|
if(batch->listBox && batch->vertical) {
|
||||||
|
UINT lines=3; SystemParametersInfoW(SPI_GETWHEELSCROLLLINES,0,&lines,0);
|
||||||
|
if(lines==WHEEL_PAGESCROLL) {
|
||||||
|
RECT client{}; GetClientRect(w,&client);
|
||||||
|
auto height=SendMessageW(w,LB_GETITEMHEIGHT,0,0);
|
||||||
|
lines=std::max<LRESULT>(1,client.bottom/std::max<LRESULT>(1,height));
|
||||||
|
}
|
||||||
|
auto delta=batch->vertical+batch->remainder;
|
||||||
|
batch->remainder=(int)(delta%WHEEL_DELTA);
|
||||||
|
auto top=SendMessageW(w,LB_GETTOPINDEX,0,0);
|
||||||
|
auto count=SendMessageW(w,LB_GETCOUNT,0,0);
|
||||||
|
auto next=std::clamp<long long>(top-(delta/WHEEL_DELTA)*lines,0,std::max<LRESULT>(0,count-1));
|
||||||
|
if(next!=top) SendMessageW(w,LB_SETTOPINDEX,(WPARAM)next,0);
|
||||||
|
batch->vertical=0;
|
||||||
|
} else if(batch->vertical) {
|
||||||
|
// Native edit/list-view/window handlers retain their own scrolling
|
||||||
|
// semantics, including system settings and partial wheel deltas.
|
||||||
|
auto delta=(short)std::clamp<long long>(batch->vertical,-32760,32760);
|
||||||
|
batch->vertical-=delta;
|
||||||
|
DefSubclassProc(w,WM_MOUSEWHEEL,MAKEWPARAM(batch->keys,(WORD)delta),batch->point);
|
||||||
|
}
|
||||||
|
if(batch->horizontal) {
|
||||||
|
auto delta=(short)std::clamp<long long>(batch->horizontal,-32760,32760);
|
||||||
|
batch->horizontal-=delta;
|
||||||
|
DefSubclassProc(w,WM_MOUSEHWHEEL,MAKEWPARAM(batch->keys,(WORD)delta),batch->point);
|
||||||
|
}
|
||||||
|
if(batch->vertical || batch->horizontal)
|
||||||
|
batch->frame=SetTimer(w,ScrollFrameTimer,16,nullptr)!=0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if(msg==WM_TIMER && wp==ScrollIdleTimer) {
|
||||||
|
if(batch->frame) return 0;
|
||||||
|
KillTimer(w,ScrollIdleTimer); batch->active=false;
|
||||||
|
SendMessageW(GetParent(w),ScrollActivity,(WPARAM)w,FALSE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if(msg==WM_NCDESTROY) {
|
||||||
|
KillTimer(w,ScrollFrameTimer); KillTimer(w,ScrollIdleTimer);
|
||||||
|
RemoveWindowSubclass(w,wheelBatchProc,id); delete batch;
|
||||||
|
}
|
||||||
|
return DefSubclassProc(w,msg,wp,lp);
|
||||||
|
}
|
||||||
|
inline void enableWheelBatching(HWND window,bool listBox=false)
|
||||||
|
{
|
||||||
|
if(!window) return;
|
||||||
|
auto *batch=new WheelBatch; batch->listBox=listBox;
|
||||||
|
if(!SetWindowSubclass(window,wheelBatchProc,ScrollFrameTimer,(DWORD_PTR)batch)) delete batch;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.19";
|
inline constexpr wchar_t AppVersion[] = L"0.6.20";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "ui_scroll.h"
|
||||||
#include "ui_geometry.h"
|
#include "ui_geometry.h"
|
||||||
#include "screenshot.h"
|
#include "screenshot.h"
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
@@ -728,16 +729,21 @@ int main(int argc, char **argv) {
|
|||||||
controlMoves=0;
|
controlMoves=0;
|
||||||
ValidateRect(settings,nullptr);
|
ValidateRect(settings,nullptr);
|
||||||
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WHEEL_DELTA),0);
|
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WHEEL_DELTA),0);
|
||||||
|
SendMessageW(settings,WM_TIMER,ks::ScrollFrameTimer,0);
|
||||||
require(controlMoves==0 && !GetUpdateRect(settings,nullptr,FALSE),"wheel at top does not move controls or erase the window");
|
require(controlMoves==0 && !GetUpdateRect(settings,nullptr,FALSE),"wheel at top does not move controls or erase the window");
|
||||||
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WORD(short(-WHEEL_DELTA/2))),0);
|
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WORD(short(-WHEEL_DELTA/2))),0);
|
||||||
|
SendMessageW(settings,WM_TIMER,ks::ScrollFrameTimer,0);
|
||||||
require(controlMoves==0,"partial wheel step does not rearrange controls");
|
require(controlMoves==0,"partial wheel step does not rearrange controls");
|
||||||
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WORD(short(-WHEEL_DELTA/2))),0);
|
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WORD(short(-WHEEL_DELTA/2))),0);
|
||||||
|
require(controlMoves==0,"wheel changes wait for the shared frame timer");
|
||||||
|
SendMessageW(settings,WM_TIMER,ks::ScrollFrameTimer,0);
|
||||||
require(controlMoves==1,"wheel scroll batches each control movement once");
|
require(controlMoves==1,"wheel scroll batches each control movement once");
|
||||||
SendMessageW(settings,WM_VSCROLL,SB_BOTTOM,0);
|
SendMessageW(settings,WM_VSCROLL,SB_BOTTOM,0);
|
||||||
SendMessageW(settings,WM_HSCROLL,SB_RIGHT,0);
|
SendMessageW(settings,WM_HSCROLL,SB_RIGHT,0);
|
||||||
controlMoves=0;
|
controlMoves=0;
|
||||||
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,-WHEEL_DELTA),0);
|
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,-WHEEL_DELTA),0);
|
||||||
SendMessageW(settings,WM_MOUSEHWHEEL,MAKEWPARAM(0,WHEEL_DELTA),0);
|
SendMessageW(settings,WM_MOUSEHWHEEL,MAKEWPARAM(0,WHEEL_DELTA),0);
|
||||||
|
SendMessageW(settings,WM_TIMER,ks::ScrollFrameTimer,0);
|
||||||
require(controlMoves==0,"wheel at bottom and right boundaries does not rearrange controls");
|
require(controlMoves==0,"wheel at bottom and right boundaries does not rearrange controls");
|
||||||
require((GetWindowLongPtrW(settings,GWL_STYLE)&WS_CLIPCHILDREN)!=0,"parent background excludes child controls");
|
require((GetWindowLongPtrW(settings,GWL_STYLE)&WS_CLIPCHILDREN)!=0,"parent background excludes child controls");
|
||||||
RemoveWindowSubclass(trackedControl,countControlMoves,99);
|
RemoveWindowSubclass(trackedControl,countControlMoves,99);
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ int main()
|
|||||||
}
|
}
|
||||||
c.marks.clear(); SendMessageW(window,WM_COMMAND,27,0);
|
c.marks.clear(); SendMessageW(window,WM_COMMAND,27,0);
|
||||||
wchar_t windowTitle[128]{}; GetWindowTextW(window,windowTitle,128);
|
wchar_t windowTitle[128]{}; GetWindowTextW(window,windowTitle,128);
|
||||||
require(std::wstring(windowTitle)==L"KSwitcher — Screenshot #1","title includes editor index without usage hints");
|
require(std::wstring(windowTitle)==ks::localBuildTitle(L"KSwitcher — Screenshot #1"),"title includes editor index and local build marker without usage hints");
|
||||||
RECT client{}; GetClientRect(window,&client);
|
RECT client{}; GetClientRect(window,&client);
|
||||||
require(c.view.bottom<=client.bottom-c.statusHeight,"image does not overlap status bar");
|
require(c.view.bottom<=client.bottom-c.statusHeight,"image does not overlap status bar");
|
||||||
ks::updateStatus(c);
|
ks::updateStatus(c);
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#include "ui_scroll.h"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int forwarded{};
|
||||||
|
long long totalDelta{};
|
||||||
|
LRESULT CALLBACK observe(HWND w,UINT msg,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
|
||||||
|
{
|
||||||
|
if(msg==WM_MOUSEWHEEL) { ++forwarded; totalDelta+=GET_WHEEL_DELTA_WPARAM(wp); }
|
||||||
|
return DefSubclassProc(w,msg,wp,lp);
|
||||||
|
}
|
||||||
|
void require(bool ok,const char *message) { if(!ok) throw std::runtime_error(message); }
|
||||||
|
void wheel(HWND w,int delta) { SendMessageW(w,WM_MOUSEWHEEL,MAKEWPARAM(0,(WORD)(short)delta),0); }
|
||||||
|
void flush(HWND w) { SendMessageW(w,WM_TIMER,ks::ScrollFrameTimer,0); }
|
||||||
|
}
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
INITCOMMONCONTROLSEX init{sizeof(init),ICC_STANDARD_CLASSES|ICC_LISTVIEW_CLASSES};
|
||||||
|
InitCommonControlsEx(&init);
|
||||||
|
auto instance=GetModuleHandleW(nullptr);
|
||||||
|
auto parent=CreateWindowW(L"STATIC",L"Scroll regression",WS_OVERLAPPEDWINDOW,0,0,500,400,nullptr,nullptr,instance,nullptr);
|
||||||
|
try {
|
||||||
|
auto list=CreateWindowW(L"LISTBOX",L"",WS_CHILD|WS_VSCROLL,0,0,300,200,parent,nullptr,instance,nullptr);
|
||||||
|
for(int i=0;i<2000;++i) SendMessageW(list,LB_ADDSTRING,0,(LPARAM)L"row");
|
||||||
|
SendMessageW(list,LB_SETCURSEL,5,0);
|
||||||
|
SendMessageW(list,LB_SETTOPINDEX,0,0);
|
||||||
|
ks::enableWheelBatching(list,true);
|
||||||
|
for(int i=0;i<1000;++i) wheel(list,-120);
|
||||||
|
require(SendMessageW(list,LB_GETTOPINDEX,0,0)==0,"burst is deferred to one frame");
|
||||||
|
flush(list);
|
||||||
|
UINT lines{}; SystemParametersInfoW(SPI_GETWHEELSCROLLLINES,0,&lines,0);
|
||||||
|
require(!lines || SendMessageW(list,LB_GETTOPINDEX,0,0)>0,"burst moves list");
|
||||||
|
require(SendMessageW(list,LB_GETCURSEL,0,0)==5,"scroll does not change selection");
|
||||||
|
for(int i=0;i<1000;++i) wheel(list,120);
|
||||||
|
flush(list);
|
||||||
|
require(SendMessageW(list,LB_GETTOPINDEX,0,0)==0,"reverse burst clamps at top");
|
||||||
|
wheel(list,-60); flush(list);
|
||||||
|
require(SendMessageW(list,LB_GETTOPINDEX,0,0)==0,"partial wheel step is retained");
|
||||||
|
wheel(list,-60); flush(list);
|
||||||
|
require(!lines || SendMessageW(list,LB_GETTOPINDEX,0,0)>0,"partial steps combine across frames");
|
||||||
|
DestroyWindow(list);
|
||||||
|
for(auto cls:{L"EDIT",WC_LISTVIEWW,L"COMBOBOX",L"STATIC"}) {
|
||||||
|
auto w=CreateWindowW(cls,L"",WS_CHILD|WS_VSCROLL,0,0,300,200,parent,nullptr,instance,nullptr);
|
||||||
|
require(w!=nullptr,"native scroll control created");
|
||||||
|
SetWindowSubclass(w,observe,1,0); ks::enableWheelBatching(w);
|
||||||
|
forwarded=0; totalDelta=0;
|
||||||
|
for(int i=0;i<100;++i) wheel(w,-120);
|
||||||
|
require(forwarded==0,"native wheel burst is batched");
|
||||||
|
flush(w);
|
||||||
|
require(forwarded==1 && totalDelta==-12000,"one native dispatch preserves burst distance");
|
||||||
|
wheel(w,60); flush(w);
|
||||||
|
require(forwarded==2 && totalDelta==-11940,"native partial steps are preserved");
|
||||||
|
forwarded=0; totalDelta=0;
|
||||||
|
for(int i=0;i<400;++i) wheel(w,-120);
|
||||||
|
flush(w); flush(w);
|
||||||
|
require(forwarded==2 && totalDelta==-48000,"large native burst is not truncated to 16 bits");
|
||||||
|
SendMessageW(w,WM_TIMER,ks::ScrollIdleTimer,0);
|
||||||
|
DestroyWindow(w);
|
||||||
|
}
|
||||||
|
DestroyWindow(parent);
|
||||||
|
std::cout<<"Passed shared wheel batching regressions (list, edit, table, combo, window)\n";
|
||||||
|
return 0;
|
||||||
|
} catch(const std::exception &e) {
|
||||||
|
DestroyWindow(parent); std::cerr<<e.what()<<'\n'; return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "autocorrect.h"
|
#include "autocorrect.h"
|
||||||
|
#include "history_preview.h"
|
||||||
#include "ocr.h"
|
#include "ocr.h"
|
||||||
#include "richtext.h"
|
#include "richtext.h"
|
||||||
#include "storage.h"
|
#include "storage.h"
|
||||||
@@ -22,6 +23,20 @@ void rejects(const std::function<void()> &f, const char *name)
|
|||||||
bool thrown = false;
|
bool thrown = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
std::map<std::wstring,int> previews;
|
||||||
|
std::set<std::wstring> visible;
|
||||||
|
for(int i=0;i<40;++i) previews[std::to_wstring(i)]=i;
|
||||||
|
for(int i=0;i<8;++i) visible.insert(std::to_wstring(i));
|
||||||
|
ks::trimHistoryPreviews(previews,visible);
|
||||||
|
require(previews.size()==32,"preview cache stays bounded");
|
||||||
|
for(const auto &id:visible) require(previews.contains(id),"visible preview survives eviction");
|
||||||
|
for(int i=0;i<40;++i) { auto id=std::to_wstring(i); previews[id]=i; visible.insert(id); }
|
||||||
|
ks::trimHistoryPreviews(previews,visible);
|
||||||
|
require(previews.size()==40,"tall viewport does not thrash visible previews");
|
||||||
|
visible.clear(); ks::trimHistoryPreviews(previews,visible);
|
||||||
|
require(previews.size()==32,"cache shrinks after leaving tall viewport");
|
||||||
|
}
|
||||||
f();
|
f();
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
|
|||||||
Reference in New Issue
Block a user