diff --git a/README.md b/README.md index 29daf2a..1b9eef9 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ In the editor, **Ctrl+C** or **Enter** copies the current result without closing The editor starts with **Pointer (V)**: select objects, move them, and edit geometry, thickness and other properties using handles and controls. Clicking empty space clears selection; dragging empty space adds nothing. Choose a drawing tool to create objects, then return to **V** to edit them. +The cursor identifies the active tool: pointer, text caret, pencil, or a crosshair with the drawing +tool symbol. Move/resize/thickness/rotation handles override it with their action cursor. Custom +cursors scale with display DPI and use a contrasting outline for light and dark images. To annotate: **N** places sequential numbered red markers with a click, and **A** draws red arrows by dragging from the tail to the tip. With **Pointer (V)**, 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 diff --git a/Release/KSwitcher-win-x64.zip b/Release/KSwitcher-win-x64.zip index 7242cf5..e4c8370 100644 Binary files a/Release/KSwitcher-win-x64.zip and b/Release/KSwitcher-win-x64.zip differ diff --git a/Release/README.md b/Release/README.md index ad71d57..38632c1 100644 --- a/Release/README.md +++ b/Release/README.md @@ -1,14 +1,14 @@ # Latest Windows release -## v0.6.6 — 2026-10-29 +## v0.6.7 — 2026-10-29 ## Changes -- Add **Pointer (V)** as the initial editor tool. Select and move objects, edit geometry and thickness with handles, and clear selection by clicking empty space. Pointer never creates objects on empty space. -- Preserve **Ctrl+drag** for drawing over existing objects and automatically linking connector endpoints. Add clearer Ctrl hints to the Arrow and Straight line buttons and status bar. -- Fix new standalone text inheriting the angle of an arrow caption. New text reuses typography only and starts horizontally; editing existing rotated text preserves its own angle. +- Show a distinct cursor for each screenshot editor tool: pointer, text caret, pencil, and crosshairs with symbols for numbers, arrows, straight lines, rectangles, circles, ellipses and highlights. +- Preserve action cursor priority for moving, resizing, changing thickness and rotating objects. +- Scale custom cursors with display DPI and add contrasting outlines for light and dark backgrounds. Tool changes refresh the cursor immediately. ## Validation -176 native checks, 440 input/layout checks at 240 DPI, screenshot editor UI integration, isolated notification registration, and 45 updater checks. UI coverage includes Pointer interactions, Ctrl drawing between markers/shapes/arrows, and standalone text after automatic or manually rotated arrow captions. +176 native checks, 440 input/layout checks at 240 DPI, screenshot editor UI integration, isolated notification registration, and 45 updater checks. Cursor coverage verifies tool selection, action overrides, hotspots and masks at 32/48/64/96 pixels. Cursor previews were visually checked on light and dark backgrounds. **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. diff --git a/Release/SHA256SUMS.txt b/Release/SHA256SUMS.txt index a57000c..942db35 100644 --- a/Release/SHA256SUMS.txt +++ b/Release/SHA256SUMS.txt @@ -1 +1 @@ -cb711ace317e60054f6498436f02bd66f10adebfe9362b8d1c106569cd27a600 KSwitcher-win-x64.zip +3c5fd91f163e54440fb144b5ef7c301695eb9d8df36dea6f807521b9c171a489 KSwitcher-win-x64.zip diff --git a/resources/app.rc b/resources/app.rc index 46864fa..2fbd3fc 100644 --- a/resources/app.rc +++ b/resources/app.rc @@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur" 1 RT_MANIFEST "app.manifest" 201 RCDATA "update.ps1" 1 VERSIONINFO - FILEVERSION 0,6,6,0 - PRODUCTVERSION 0,6,6,0 + FILEVERSION 0,6,7,0 + PRODUCTVERSION 0,6,7,0 FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_APP BEGIN @@ -20,7 +20,7 @@ BEGIN BEGIN VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility" VALUE "ProductName", "KSwitcher" - VALUE "FileVersion", "0.6.6" + VALUE "FileVersion", "0.6.7" END END BLOCK "VarFileInfo" diff --git a/src/screenshot.cpp b/src/screenshot.cpp index 60b3860..91429c8 100644 --- a/src/screenshot.cpp +++ b/src/screenshot.cpp @@ -123,6 +123,44 @@ bool chooseTextFont(HWND owner,Mark &m) m.fontWeight=font.lfWeight; m.fontItalic=font.lfItalic!=0; return true; } +constexpr int CursorTools[]{10,11,22,23,24,25,19}; +HCURSOR makeToolCursor(int tool,int size) +{ + // Monochrome AND/XOR cursor: white outline keeps the glyph visible on dark images. + auto bitmap=CreateBitmap(size,size*2,1,1,nullptr); + auto dc=CreateCompatibleDC(nullptr); + if(!bitmap || !dc) { if(bitmap) DeleteObject(bitmap); if(dc) DeleteDC(dc); return nullptr; } + auto oldBitmap=SelectObject(dc,bitmap); + PatBlt(dc,0,0,size,size,WHITENESS); PatBlt(dc,0,size,size,size,BLACKNESS); + auto oldBrush=SelectObject(dc,GetStockObject(NULL_BRUSH)); + auto draw=[&](int offset,int width,COLORREF color) { + auto pen=CreatePen(PS_SOLID,width,color); auto oldPen=SelectObject(dc,pen); + auto x=[&](int v){return MulDiv(v,size,32);}; + auto y=[&](int v){return offset+MulDiv(v,size,32);}; + auto line=[&](int a,int b,int c,int d){MoveToEx(dc,x(a),y(b),nullptr); LineTo(dc,x(c),y(d));}; + line(1,7,13,7); line(7,1,7,13); // The cross center is the exact drawing point. + switch(tool) { + case 10: + Ellipse(dc,x(17),y(17),x(30),y(30)); + line(22,21,24,20); line(24,20,24,27); line(21,27,27,27); break; + case 11: line(18,29,29,18); line(22,18,29,18); line(29,18,29,25); break; + case 22: line(18,29,29,18); break; + case 23: Rectangle(dc,x(17),y(18),x(30),y(29)); break; + case 24: Ellipse(dc,x(17),y(17),x(30),y(30)); break; + case 25: Ellipse(dc,x(16),y(20),x(31),y(28)); break; + case 19: + line(18,25,25,18); line(25,18,29,22); line(29,22,22,29); + line(22,29,18,25); line(18,25,17,30); line(17,30,22,29); break; + } + SelectObject(dc,oldPen); DeleteObject(pen); + }; + int outline=std::max(3,MulDiv(4,size,32)),stroke=std::max(1,MulDiv(2,size,32)); + draw(0,outline,RGB(0,0,0)); + draw(size,outline,RGB(255,255,255)); draw(size,stroke,RGB(0,0,0)); + SelectObject(dc,oldBrush); SelectObject(dc,oldBitmap); DeleteDC(dc); + ICONINFO info{}; info.fIcon=FALSE; info.xHotspot=info.yHotspot=MulDiv(7,size,32); info.hbmMask=bitmap; + auto cursor=(HCURSOR)CreateIconIndirect(&info); DeleteObject(bitmap); return cursor; +} struct Capture { ScreenshotHost &app; @@ -159,6 +197,7 @@ struct Capture HICON toolIcons[3][15]{}; int iconSize{}; HCURSOR pencilCursor{}; + HCURSOR drawingCursors[std::size(CursorTools)]{}; int pencilCursorSize{}; int unit = 1, panelHeight = 1, statusHeight = 0; int hoveredTool=0; @@ -167,6 +206,7 @@ struct Capture explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {} ~Capture() { if(pencilCursor) DestroyCursor(pencilCursor); + for(auto cursor:drawingCursors) if(cursor) DestroyCursor(cursor); if (font) DeleteObject(font); if(inlineFont) DeleteObject(inlineFont); for(auto &variant:toolIcons) for(auto icon:variant) if(icon) DestroyIcon(icon); @@ -523,6 +563,11 @@ void layout(HWND w, Capture &c) int dpi = GetDpiForWindow(w); int cursorSize=GetSystemMetricsForDpi(SM_CXCURSOR,dpi); if(cursorSize!=c.pencilCursorSize) { + for(size_t i=0;i markAt(Capture &c, POINT p) } return {-1,MarkPart::None}; } +HCURSOR toolCursor(const Capture &c) +{ + if(c.pointer) return LoadCursorW(nullptr,IDC_ARROW); + if(c.textTool) return LoadCursorW(nullptr,IDC_IBEAM); + if(c.freehand && c.pencilCursor) return c.pencilCursor; + int tool=c.highlight?19:c.straight?22:c.shape!=ScreenshotShape::None?22+int(c.shape):c.arrow?11:10; + for(size_t i=0;i hit,bool inside) +{ + if(!c.editing) return LoadCursorW(nullptr,IDC_CROSS); + LPCWSTR cursor=nullptr; + if(hit.second==MarkPart::Body || hit.second==MarkPart::Label) cursor=IDC_SIZEALL; + else if(hit.second==MarkPart::Rotation) cursor=IDC_HAND; + else if(hit.second==MarkPart::Radius) cursor=IDC_SIZEWE; + else if(hit.second==MarkPart::Start || hit.second==MarkPart::End || hit.second==MarkPart::Thickness) { + const auto &mark=c.marks[hit.first]; + auto [from,to]=hit.second==MarkPart::Thickness?thicknessSegment(mark):std::pair{mark.from,mark.to}; + double angle=std::atan2(double(to.y-from.y),double(to.x-from.x)); + if(hit.second==MarkPart::Thickness) angle+=3.141592653589793/2; + int axis=(int(std::lround(angle/(3.141592653589793/4)))+8)%4; + const LPCWSTR cursors[]{IDC_SIZEWE,IDC_SIZENWSE,IDC_SIZENS,IDC_SIZENESW}; + cursor=cursors[axis]; + } + return cursor?LoadCursorW(nullptr,cursor):inside?toolCursor(c):LoadCursorW(nullptr,IDC_ARROW); +} void captureCursor(HWND w, Capture &c) { - LPCWSTR cursor=c.editing && c.pointer?IDC_ARROW:IDC_CROSS; - if(c.editing) - { - POINT p; GetCursorPos(&p); ScreenToClient(w,&p); - auto hit=c.dragging?std::pair{c.selected,c.part}:markAt(c,imagePoint(c,p,false)); - if(!c.dragging && !PtInRect(&c.view,p) && hit.second!=MarkPart::Thickness && hit.second!=MarkPart::Rotation) hit={-1,MarkPart::None}; - if(!c.dragging && !PtInRect(&c.view,p)) cursor=IDC_ARROW; - if(hit.second==MarkPart::Body || hit.second==MarkPart::Label) cursor=IDC_SIZEALL; - else if(hit.second==MarkPart::Rotation) cursor=IDC_HAND; - else if(hit.second==MarkPart::Radius) cursor=IDC_SIZEWE; - else if(hit.second==MarkPart::Start || hit.second==MarkPart::End || hit.second==MarkPart::Thickness) - { - const auto &mark=c.marks[hit.first]; - auto [from,to]=hit.second==MarkPart::Thickness?thicknessSegment(mark):std::pair{mark.from,mark.to}; - double angle=std::atan2(double(to.y-from.y),double(to.x-from.x)); - if(hit.second==MarkPart::Thickness) angle+=3.141592653589793/2; - int axis=(int(std::lround(angle/(3.141592653589793/4)))+8)%4; - const LPCWSTR cursors[]{IDC_SIZEWE,IDC_SIZENWSE,IDC_SIZENS,IDC_SIZENESW}; - cursor=cursors[axis]; - } - } - SetCursor(c.editing && c.freehand && cursor==IDC_CROSS && c.pencilCursor?c.pencilCursor:LoadCursorW(nullptr,cursor)); + POINT p; GetCursorPos(&p); ScreenToClient(w,&p); + bool inside=PtInRect(&c.view,p)!=FALSE; + auto hit=c.editing?(c.dragging?std::pair{c.selected,c.part}:markAt(c,imagePoint(c,p,false))):std::pair{-1,MarkPart::None}; + if(!c.dragging && !inside && hit.second!=MarkPart::Thickness && hit.second!=MarkPart::Rotation) hit={-1,MarkPart::None}; + SetCursor(actionCursor(c,hit,inside || c.dragging)); if(c.editing) { POINT p; GetCursorPos(&p); ScreenToClient(w,&p); c.hoveredPart=markAt(c,imagePoint(c,p,false)).second; @@ -987,7 +1043,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) for(int id=22;id<=27;++id) InvalidateRect(GetDlgItem(w,id),nullptr,FALSE); } - SetFocus(w); InvalidateRect(w,nullptr,FALSE); return 0; + SetFocus(w); captureCursor(w,*c); InvalidateRect(w,nullptr,FALSE); return 0; case WM_KEYDOWN: if (wp==VK_ESCAPE) { if(!c->editing) DestroyWindow(w); // Cancel region selection only. diff --git a/src/update.h b/src/update.h index 239e849..c96abf2 100644 --- a/src/update.h +++ b/src/update.h @@ -2,7 +2,7 @@ #include "platform.h" namespace ks { -inline constexpr wchar_t AppVersion[] = L"0.6.6"; +inline constexpr wchar_t AppVersion[] = L"0.6.7"; inline constexpr UINT UpdateMessage = WM_APP + 7; inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000; struct UpdateResult diff --git a/tests/screenshot_ui.cpp b/tests/screenshot_ui.cpp index 17362b9..0bff004 100644 --- a/tests/screenshot_ui.cpp +++ b/tests/screenshot_ui.cpp @@ -31,6 +31,48 @@ int main() if(info.hbmColor) DeleteObject(info.hbmColor); DestroyCursor(cursor); } + ks::Surface cursorPreview(784,448); + PatBlt(cursorPreview.dc,0,0,784,448,WHITENESS); + int row=0; + for(int size:{32,48,64,96}) { + int column=0; + for(int tool:ks::CursorTools) { + auto cursor=ks::makeToolCursor(tool,size); ICONINFO info{}; + require(cursor && GetIconInfo(cursor,&info) && !info.fIcon,"tool cursors support standard and high DPI"); + require(info.xHotspot==UINT(MulDiv(7,size,32)) && info.yHotspot==info.xHotspot,"tool cursor hotspot stays on crosshair center"); + BITMAP mask{}; GetObjectW(info.hbmMask,sizeof(mask),&mask); + require(mask.bmWidth==size && mask.bmHeight==size*2,"cursor transparency mask matches requested DPI"); + if(column%2) { + RECT tile{column*112,row*112,(column+1)*112,(row+1)*112}; + auto dark=CreateSolidBrush(RGB(40,40,40)); FillRect(cursorPreview.dc,&tile,dark); DeleteObject(dark); + } + DrawIconEx(cursorPreview.dc,column*112,row*112,cursor,size,size,0,nullptr,DI_NORMAL); + if(info.hbmMask) DeleteObject(info.hbmMask); + if(info.hbmColor) DeleteObject(info.hbmColor); + DestroyCursor(cursor); ++column; + } + ++row; + } + auto dib=ks::screenshotClip(cursorPreview.dc,784,448,{}).formats[0].data; + BITMAPFILEHEADER bmp{}; bmp.bfType=0x4d42; bmp.bfOffBits=sizeof(bmp)+sizeof(BITMAPINFOHEADER); bmp.bfSize=DWORD(sizeof(bmp)+dib.size()); + ks::Bytes preview(sizeof(bmp)); memcpy(preview.data(),&bmp,sizeof(bmp)); preview.insert(preview.end(),dib.begin(),dib.end()); + ks::atomicWrite(L"build/cursor-preview.bmp",preview); + for(int tool:{27,26,21,10,11,22,23,24,25,19}) { + SendMessageW(window,WM_COMMAND,tool,0); + HCURSOR expected=nullptr; + if(tool==27) expected=LoadCursorW(nullptr,IDC_ARROW); + else if(tool==26) expected=LoadCursorW(nullptr,IDC_IBEAM); + else if(tool==21) expected=c.pencilCursor; + else for(size_t i=0;i