Compare commits

...

2 Commits

8 changed files with 241 additions and 57 deletions
+12 -3
View File
@@ -90,8 +90,14 @@ broker can load them even when the profile directory is inaccessible to it.
Notifications expire after 24 hours. Cache cleanup runs on capture/open; each cache is bounded to Notifications expire after 24 hours. Cache cleanup runs on capture/open; each cache is bounded to
40 files and 256 MiB. Expired or evicted notifications cannot reopen their screenshot. 40 files and 256 MiB. Expired or evicted notifications cannot reopen their screenshot.
In the editor, **Ctrl+C** or **Enter** copies the current result without closing the window. In the editor, **Ctrl+C** or **Enter** copies the current result without closing the window.
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 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. 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 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.
Select an arrow and drag the extra handle beside its midpoint perpendicular to the line to change Select an arrow and drag the extra handle beside its midpoint perpendicular to the line to change
@@ -102,8 +108,9 @@ and update linked arrows. **Ctrl+D** clones the selected object with a small off
gets the smallest available positive number, just like a newly placed number: gaps left by deletion gets the smallest available positive number, just like a newly placed number: gaps left by deletion
are filled first, without renumbering existing objects. A cloned arrow keeps its style but starts without endpoint links. are filled first, without renumbering existing objects. A cloned arrow keeps its style but starts without endpoint links.
Arrow and line endpoints can attach to numbered markers, rectangles, circles, ellipses, text blocks, Arrow and line endpoints can attach to numbered markers, rectangles, circles, ellipses, text blocks,
and other arrows or strokes. Drop an endpoint onto the target; use **Ctrl+drag** to start a new and other arrows or strokes. In Arrow or Straight line mode, hold **Ctrl** and drag from one object
connector over an existing object. Attached endpoints follow movement and resizing; text attachments to another to draw over them and automatically attach both endpoints. Without Ctrl, existing objects
can still be selected and edited as before. With Pointer, drag an existing endpoint onto a target to attach it. Attached endpoints follow movement and resizing; text attachments
also follow rotation. Arrow-to-arrow attachments retain their relative position along the target line. also follow rotation. Arrow-to-arrow attachments retain their relative position along the target line.
Clicking a connector or adding/editing its caption preserves its links. Moving the whole connector Clicking a connector or adding/editing its caption preserves its links. Moving the whole connector
intentionally detaches its endpoints; moving one endpoint detaches only that end. Deleting a target intentionally detaches its endpoints; moving one endpoint detaches only that end. Deleting a target
@@ -136,6 +143,8 @@ The mouse wheel over text adjusts its font size. Text supports color, dragging,
Delete and Undo/Redo. Arrow keys move the selected object by one source-image pixel; Delete and Undo/Redo. Arrow keys move the selected object by one source-image pixel;
selecting an arrow caption moves only that caption. Repositioning a caption preserves its selecting an arrow caption moves only that caption. Repositioning a caption preserves its
automatic angle; only dragging the rotation handle switches to a fixed angle. automatic angle; only dragging the rotation handle switches to a fixed angle.
New standalone text starts horizontally, even after editing an angled arrow caption. Font styling
is reused; rotation belongs to each individual object.
Text has a rotation handle above its selection. Drag to rotate; hold Shift to snap to 15-degree steps. Text has a rotation handle above its selection. Drag to rotate; hold Shift to snap to 15-degree steps.
Double-click an arrow, press F2, or use the label icon to add/edit its caption. Captions follow Double-click an arrow, press F2, or use the label icon to add/edit its caption. Captions follow
the arrow when moved or resized; drag the caption to adjust its offset. By default, the caption the arrow when moved or resized; drag the caption to adjust its offset. By default, the caption
Binary file not shown.
+5 -9
View File
@@ -1,18 +1,14 @@
# Latest Windows release # Latest Windows release
## v0.6.5 — 2026-10-29 ## v0.6.7 — 2026-10-29
## Changes ## Changes
- Preserve endpoint links when selecting a connector or adding/editing its caption through double-click, F2 or the caption button. A click without movement no longer detaches the connector. - 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.
- Attach arrow and line endpoints to rectangles, circles, ellipses, text blocks, other arrows/lines, and freehand/highlight strokes, in addition to numbered markers. - Preserve action cursor priority for moving, resizing, changing thickness and rotating objects.
- Keep attachments aligned as targets move or resize; text attachments also follow rotation. Arrow-to-arrow chains resolve in dependency order. - Scale custom cursors with display DPI and add contrasting outlines for light and dark backgrounds. Tool changes refresh the cursor immediately.
- Preserve links through Undo/Redo. Deleted targets leave endpoints at their last position; clones start without links. Self-links and circular dependencies are rejected.
## Usage
Drop a connector endpoint onto the target. Use **Ctrl+drag** to start a new connector over an existing object. Moving a whole connector detaches both of its endpoints; dragging one endpoint detaches only that end.
## Validation ## Validation
176 native checks, 440 input/layout checks at 240 DPI, screenshot editor UI integration, isolated notification registration, and 45 updater checks. Coverage includes caption editing, shape-arrow-arrow chains, movement, resize, text rotation, deletion, cloning, cycle rejection, and Undo/Redo. 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 **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. v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
+1 -1
View File
@@ -1 +1 @@
bea45fdb33657fdfd5b065cdf10a3c358febdd5e895672dd106ec5ef2dd16c9a KSwitcher-win-x64.zip 3c5fd91f163e54440fb144b5ef7c301695eb9d8df36dea6f807521b9c171a489 KSwitcher-win-x64.zip
+3 -3
View File
@@ -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,5,0 FILEVERSION 0,6,7,0
PRODUCTVERSION 0,6,5,0 PRODUCTVERSION 0,6,7,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.5" VALUE "FileVersion", "0.6.7"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+97 -28
View File
@@ -53,7 +53,7 @@ bool confirmScreenshotClose(HWND owner)
// If Windows cannot display the confirmation, keep the screenshot open. // If Windows cannot display the confirmation, keep the screenshot open.
return SUCCEEDED(result) && button==IDYES; return SUCCEEDED(result) && button==IDYES;
} }
constexpr int ToolIds[]{10,11,22,23,24,25,21,19,26,20,12,18,13}; constexpr int ToolIds[]{27,10,11,22,23,24,25,21,19,26,20,12,18,13};
struct Surface struct Surface
{ {
HDC dc = CreateCompatibleDC(nullptr); HDC dc = CreateCompatibleDC(nullptr);
@@ -123,6 +123,44 @@ bool chooseTextFont(HWND owner,Mark &m)
m.fontWeight=font.lfWeight; m.fontItalic=font.lfItalic!=0; m.fontWeight=font.lfWeight; m.fontItalic=font.lfItalic!=0;
return true; 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 struct Capture
{ {
ScreenshotHost &app; ScreenshotHost &app;
@@ -131,7 +169,7 @@ struct Capture
bool editing{}, dragging{}, arrow{}, owned{}, copied{}; bool editing{}, dragging{}, arrow{}, owned{}, copied{};
bool autoOpenEditor; bool autoOpenEditor;
bool highlight{}; bool highlight{};
bool freehand=true; bool pointer=true, freehand=false;
bool straight{}, textTool{}; bool straight{}, textTool{};
Mark textDefaults{}; Mark textDefaults{};
ScreenshotShape shape=ScreenshotShape::None; ScreenshotShape shape=ScreenshotShape::None;
@@ -159,6 +197,7 @@ struct Capture
HICON toolIcons[3][15]{}; HICON toolIcons[3][15]{};
int iconSize{}; int iconSize{};
HCURSOR pencilCursor{}; HCURSOR pencilCursor{};
HCURSOR drawingCursors[std::size(CursorTools)]{};
int pencilCursorSize{}; int pencilCursorSize{};
int unit = 1, panelHeight = 1, statusHeight = 0; int unit = 1, panelHeight = 1, statusHeight = 0;
int hoveredTool=0; int hoveredTool=0;
@@ -167,6 +206,7 @@ struct Capture
explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {} explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {}
~Capture() { ~Capture() {
if(pencilCursor) DestroyCursor(pencilCursor); if(pencilCursor) DestroyCursor(pencilCursor);
for(auto cursor:drawingCursors) if(cursor) DestroyCursor(cursor);
if (font) DeleteObject(font); if (font) DeleteObject(font);
if(inlineFont) DeleteObject(inlineFont); if(inlineFont) DeleteObject(inlineFont);
for(auto &variant:toolIcons) for(auto icon:variant) if(icon) DestroyIcon(icon); for(auto &variant:toolIcons) for(auto icon:variant) if(icon) DestroyIcon(icon);
@@ -207,11 +247,12 @@ void updateStatus(Capture &c)
else if(m.isText()) text=L"Text: arrows = 1 px | F2 = edit | Round handle = rotate; Shift = 15 degrees"; else if(m.isText()) text=L"Text: arrows = 1 px | F2 = edit | Round handle = rotate; Shift = 15 degrees";
else text=L"Arrows = 1 px | Del = delete | Ctrl+D = clone | Ctrl+Z / Ctrl+Y = undo / redo"; else text=L"Arrows = 1 px | Del = delete | Ctrl+D = clone | Ctrl+Z / Ctrl+Y = undo / redo";
} }
else if(c.pointer) text=L"Pointer (V): select and edit objects | Drag handles = geometry or thickness | Empty space clears selection";
else if(c.textTool) text=L"Text (T): click the image to type | Double-click existing text to edit"; else if(c.textTool) text=L"Text (T): click the image to type | Double-click existing text to edit";
else if(c.highlight) text=L"Highlight (H): drag to paint | Ctrl+drag = draw over an object"; else if(c.highlight) text=L"Highlight (H): drag to paint | Ctrl+drag = paint over an object | V = pointer";
else if(c.freehand) text=L"Freehand (F): drag to draw | Ctrl+drag = draw over an object"; else if(c.freehand) text=L"Freehand (F): drag to draw | Ctrl+drag = draw over an object | V = pointer";
else if(c.arrow) text=L"Drag to draw | Ctrl+drag = draw over an object | F2 on a line = caption"; else if(c.arrow) text=L"Drag to draw | Ctrl+drag over objects = linked connector | V = pointer";
else text=L"Number (N): click to add | Drag objects to move | Ctrl+C = copy screenshot"; else text=L"Number (N): click to add | V = select and edit | Ctrl+C = copy screenshot";
if(text!=c.statusText) { if(text!=c.statusText) {
c.statusText=std::move(text); c.statusText=std::move(text);
SendMessageW(status,SB_SETTEXTW,0,(LPARAM)c.statusText.c_str()); SendMessageW(status,SB_SETTEXTW,0,(LPARAM)c.statusText.c_str());
@@ -252,6 +293,15 @@ void positionInlineText(Capture &c)
int height=std::min(std::max(40,MulDiv(bounds.bottom-bounds.top,viewWidth,c.image->width)+24),std::max(32,int(client.bottom)-c.statusHeight-y)); int height=std::min(std::max(40,MulDiv(bounds.bottom-bounds.top,viewWidth,c.image->width)+24),std::max(32,int(client.bottom)-c.statusHeight-y));
SetWindowPos(c.inlineText,nullptr,x-2,y-2,width,height,SWP_NOZORDER|SWP_NOACTIVATE); SetWindowPos(c.inlineText,nullptr,x-2,y-2,width,height,SWP_NOZORDER|SWP_NOACTIVATE);
} }
Mark textStyleDefaults(const Mark &source)
{
// Reuse typography only, never an object's angle, position, identity or links.
Mark style{};
style.fontName=source.fontName; style.fontSize=source.fontSize;
style.fontWeight=source.fontWeight; style.fontItalic=source.fontItalic;
style.color=source.color;
return style;
}
void finishInlineText(HWND w,Capture &c,bool commit) void finishInlineText(HWND w,Capture &c,bool commit)
{ {
if(!c.inlineText) return; if(!c.inlineText) return;
@@ -274,7 +324,7 @@ void finishInlineText(HWND w,Capture &c,bool commit)
} else if(!empty) { } else if(!empty) {
c.remember(c.marks); c.marks.push_back(c.textDraft); c.selected=int(c.marks.size())-1; c.selectedLabel=false; c.remember(c.marks); c.marks.push_back(c.textDraft); c.selected=int(c.marks.size())-1; c.selectedLabel=false;
} }
if(!empty) c.textDefaults=c.textDraft; if(!empty) c.textDefaults=textStyleDefaults(c.textDraft);
} }
if(commit) resolveScreenshotLinks(c.marks); if(commit) resolveScreenshotLinks(c.marks);
c.textIndex=-1; c.editingLabel=false; c.textIndex=-1; c.editingLabel=false;
@@ -513,6 +563,11 @@ void layout(HWND w, Capture &c)
int dpi = GetDpiForWindow(w); int dpi = GetDpiForWindow(w);
int cursorSize=GetSystemMetricsForDpi(SM_CXCURSOR,dpi); int cursorSize=GetSystemMetricsForDpi(SM_CXCURSOR,dpi);
if(cursorSize!=c.pencilCursorSize) { if(cursorSize!=c.pencilCursorSize) {
for(size_t i=0;i<std::size(CursorTools);++i) {
auto cursor=makeToolCursor(CursorTools[i],cursorSize);
if(c.drawingCursors[i]) DestroyCursor(c.drawingCursors[i]);
c.drawingCursors[i]=cursor;
}
auto cursor=(HCURSOR)LoadImageW(c.app.instance,MAKEINTRESOURCEW(IDC_SCREENSHOT_PENCIL),IMAGE_CURSOR,cursorSize,cursorSize,0); auto cursor=(HCURSOR)LoadImageW(c.app.instance,MAKEINTRESOURCEW(IDC_SCREENSHOT_PENCIL),IMAGE_CURSOR,cursorSize,cursorSize,0);
if(cursor) { if(cursor) {
if(c.pencilCursor) DestroyCursor(c.pencilCursor); if(c.pencilCursor) DestroyCursor(c.pencilCursor);
@@ -539,7 +594,7 @@ void layout(HWND w, Capture &c)
RECT statusRect{}; GetWindowRect(status,&statusRect); RECT statusRect{}; GetWindowRect(status,&statusRect);
c.statusHeight=statusRect.bottom-statusRect.top; c.statusHeight=statusRect.bottom-statusRect.top;
r.bottom-=c.statusHeight; r.bottom-=c.statusHeight;
const int positions[]{8,44,80,116,152,188,224,260,296,332,380,416,464}; const int positions[]{8,44,80,116,152,188,224,260,296,332,368,416,452,500};
static_assert(std::size(positions)==std::size(ToolIds)); static_assert(std::size(positions)==std::size(ToolIds));
for (size_t i = 0; i < std::size(ToolIds); ++i) for (size_t i = 0; i < std::size(ToolIds); ++i)
{ {
@@ -608,20 +663,24 @@ std::pair<int,MarkPart> markAt(Capture &c, POINT p)
} }
return {-1,MarkPart::None}; return {-1,MarkPart::None};
} }
void captureCursor(HWND w, Capture &c) HCURSOR toolCursor(const Capture &c)
{ {
LPCWSTR cursor=IDC_CROSS; if(c.pointer) return LoadCursorW(nullptr,IDC_ARROW);
if(c.editing) if(c.textTool) return LoadCursorW(nullptr,IDC_IBEAM);
{ if(c.freehand && c.pencilCursor) return c.pencilCursor;
POINT p; GetCursorPos(&p); ScreenToClient(w,&p); int tool=c.highlight?19:c.straight?22:c.shape!=ScreenshotShape::None?22+int(c.shape):c.arrow?11:10;
auto hit=c.dragging?std::pair{c.selected,c.part}:markAt(c,imagePoint(c,p,false)); for(size_t i=0;i<std::size(CursorTools);++i)
if(!c.dragging && !PtInRect(&c.view,p) && hit.second!=MarkPart::Thickness && hit.second!=MarkPart::Rotation) hit={-1,MarkPart::None}; if(CursorTools[i]==tool && c.drawingCursors[i]) return c.drawingCursors[i];
if(!c.dragging && !PtInRect(&c.view,p)) cursor=IDC_ARROW; return LoadCursorW(nullptr,IDC_CROSS);
}
HCURSOR actionCursor(const Capture &c,std::pair<int,MarkPart> 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; 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::Rotation) cursor=IDC_HAND;
else if(hit.second==MarkPart::Radius) cursor=IDC_SIZEWE; else if(hit.second==MarkPart::Radius) cursor=IDC_SIZEWE;
else if(hit.second==MarkPart::Start || hit.second==MarkPart::End || hit.second==MarkPart::Thickness) else if(hit.second==MarkPart::Start || hit.second==MarkPart::End || hit.second==MarkPart::Thickness) {
{
const auto &mark=c.marks[hit.first]; const auto &mark=c.marks[hit.first];
auto [from,to]=hit.second==MarkPart::Thickness?thicknessSegment(mark):std::pair{mark.from,mark.to}; 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)); double angle=std::atan2(double(to.y-from.y),double(to.x-from.x));
@@ -630,8 +689,15 @@ void captureCursor(HWND w, Capture &c)
const LPCWSTR cursors[]{IDC_SIZEWE,IDC_SIZENWSE,IDC_SIZENS,IDC_SIZENESW}; const LPCWSTR cursors[]{IDC_SIZEWE,IDC_SIZENWSE,IDC_SIZENS,IDC_SIZENESW};
cursor=cursors[axis]; cursor=cursors[axis];
} }
} return cursor?LoadCursorW(nullptr,cursor):inside?toolCursor(c):LoadCursorW(nullptr,IDC_ARROW);
SetCursor(c.editing && c.freehand && cursor==IDC_CROSS && c.pencilCursor?c.pencilCursor:LoadCursorW(nullptr,cursor)); }
void captureCursor(HWND w, Capture &c)
{
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) { if(c.editing) {
POINT p; GetCursorPos(&p); ScreenToClient(w,&p); POINT p; GetCursorPos(&p); ScreenToClient(w,&p);
c.hoveredPart=markAt(c,imagePoint(c,p,false)).second; c.hoveredPart=markAt(c,imagePoint(c,p,false)).second;
@@ -698,7 +764,7 @@ void drawTool(const DRAWITEMSTRUCT &item, const Capture &c)
{ {
auto dc=item.hDC; auto dc=item.hDC;
RECT r=item.rcItem; RECT r=item.rcItem;
bool active=(item.CtlID==10 && !c.arrow && !c.highlight && !c.freehand && !c.textTool)||(item.CtlID==11 && c.arrow && !c.straight && c.shape==ScreenshotShape::None)|| bool active=(item.CtlID==27 && c.pointer)||(item.CtlID==10 && !c.pointer && !c.arrow && !c.highlight && !c.freehand && !c.textTool)||(item.CtlID==11 && c.arrow && !c.straight && c.shape==ScreenshotShape::None)||
(item.CtlID==19 && c.highlight)||(item.CtlID==21 && c.freehand)|| (item.CtlID==19 && c.highlight)||(item.CtlID==21 && c.freehand)||
(item.CtlID==22 && c.straight)||(item.CtlID>=23 && item.CtlID<=25 && int(c.shape)==int(item.CtlID)-22)||(item.CtlID==26 && c.textTool); (item.CtlID==22 && c.straight)||(item.CtlID>=23 && item.CtlID<=25 && int(c.shape)==int(item.CtlID)-22)||(item.CtlID==26 && c.textTool);
bool disabled=item.itemState&ODS_DISABLED; bool disabled=item.itemState&ODS_DISABLED;
@@ -714,7 +780,7 @@ void drawTool(const DRAWITEMSTRUCT &item, const Capture &c)
RoundRect(dc,r.left,r.top,r.right,r.bottom,corner,corner); RoundRect(dc,r.left,r.top,r.right,r.bottom,corner,corner);
SelectObject(dc,oldPen); SelectObject(dc,oldBrush); DeleteObject(pen); DeleteObject(brush); SelectObject(dc,oldPen); SelectObject(dc,oldBrush); DeleteObject(pen); DeleteObject(brush);
int iconIndex=item.CtlID==35?13:item.CtlID==34?14:item.CtlID>=22?int(item.CtlID)-13:item.CtlID==18?5:item.CtlID==19?6:item.CtlID==20?7:item.CtlID==21?8:item.CtlID-10; int iconIndex=item.CtlID==35?13:item.CtlID==34?14:item.CtlID>=22?int(item.CtlID)-13:item.CtlID==18?5:item.CtlID==19?6:item.CtlID==20?7:item.CtlID==21?8:item.CtlID-10;
auto icon=c.toolIcons[disabled?2:active?1:0][iconIndex]; auto icon=item.CtlID==27?(HICON)LoadCursorW(nullptr,IDC_ARROW):c.toolIcons[disabled?2:active?1:0][iconIndex];
if(icon) DrawIconEx(dc,(r.right-c.iconSize)/2,(r.bottom-c.iconSize)/2,icon,c.iconSize,c.iconSize,0,nullptr,DI_NORMAL); if(icon) DrawIconEx(dc,(r.right-c.iconSize)/2,(r.bottom-c.iconSize)/2,icon,c.iconSize,c.iconSize,0,nullptr,DI_NORMAL);
if(item.CtlID==20) { if(item.CtlID==20) {
auto color=c.selected>=0?c.marks[c.selected].color:c.currentColor; auto color=c.selected>=0?c.marks[c.selected].color:c.currentColor;
@@ -775,7 +841,7 @@ void edit(HWND w, Capture &c)
InitCommonControlsEx(&statusControls); InitCommonControlsEx(&statusControls);
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS, CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
0,0,0,0,w,(HMENU)50,c.app.instance,nullptr); 0,0,0,0,w,(HMENU)50,c.app.instance,nullptr);
const wchar_t *labels[]{L"Number (N)", L"Arrow (A)", L"Straight line (L)", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Ctrl+drag to draw over an object", L"Highlight (H); Ctrl+drag to paint over an object", const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Ctrl+drag to draw over an object", L"Highlight (H); Ctrl+drag to paint over an object",
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"}; L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
static_assert(std::size(labels)==std::size(ToolIds)); static_assert(std::size(labels)==std::size(ToolIds));
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX, auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
@@ -867,7 +933,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
} }
break; break;
case WM_DRAWITEM: case WM_DRAWITEM:
if((wp>=10 && wp<=13) || (wp>=18 && wp<=26) || wp==34 || wp==35) { drawTool(*reinterpret_cast<DRAWITEMSTRUCT *>(lp),*c); return TRUE; } if((wp>=10 && wp<=13) || (wp>=18 && wp<=27) || wp==34 || wp==35) { drawTool(*reinterpret_cast<DRAWITEMSTRUCT *>(lp),*c); return TRUE; }
break; break;
case WM_CTLCOLORSTATIC: case WM_CTLCOLORSTATIC:
SetBkColor((HDC)wp,RGB(255,255,255)); SetTextColor((HDC)wp,RGB(51,65,85)); SetBkColor((HDC)wp,RGB(255,255,255)); SetTextColor((HDC)wp,RGB(51,65,85));
@@ -894,7 +960,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
auto draft=c->marks[c->selected]; auto draft=c->marks[c->selected];
if(chooseTextFont(w,draft)) { if(chooseTextFont(w,draft)) {
c->remember(c->marks); c->marks[c->selected]=draft; c->remember(c->marks); c->marks[c->selected]=draft;
c->textDefaults=draft.isLine()?screenshotArrowLabel(draft):draft; c->textDefaults=textStyleDefaults(draft);
resolveScreenshotLinks(c->marks); resolveScreenshotLinks(c->marks);
} }
} else chooseTextFont(w,c->textDefaults); } else chooseTextFont(w,c->textDefaults);
@@ -958,8 +1024,9 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if(changed) { c->copied=false; c->selected=-1; c->selectedLabel=false; c->wheelDelta=0; c->wheelTarget=-1; } if(changed) { c->copied=false; c->selected=-1; c->selectedLabel=false; c->wheelDelta=0; c->wheelTarget=-1; }
c->syncHistory(); c->syncHistory();
} }
if (LOWORD(wp)==10 || LOWORD(wp)==11 || LOWORD(wp)==19 || (LOWORD(wp)>=21 && LOWORD(wp)<=26)) if (LOWORD(wp)==10 || LOWORD(wp)==11 || LOWORD(wp)==19 || (LOWORD(wp)>=21 && LOWORD(wp)<=27))
{ {
c->pointer=LOWORD(wp)==27;
c->arrow = LOWORD(wp)==11 || (LOWORD(wp)>=22 && LOWORD(wp)<=25); c->arrow = LOWORD(wp)==11 || (LOWORD(wp)>=22 && LOWORD(wp)<=25);
c->straight=LOWORD(wp)==22; c->straight=LOWORD(wp)==22;
c->textTool=LOWORD(wp)==26; c->textTool=LOWORD(wp)==26;
@@ -973,10 +1040,10 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
InvalidateRect(GetDlgItem(w,11),nullptr,FALSE); InvalidateRect(GetDlgItem(w,11),nullptr,FALSE);
InvalidateRect(GetDlgItem(w,19),nullptr,FALSE); InvalidateRect(GetDlgItem(w,19),nullptr,FALSE);
InvalidateRect(GetDlgItem(w,21),nullptr,FALSE); InvalidateRect(GetDlgItem(w,21),nullptr,FALSE);
for(int id=22;id<=26;++id) InvalidateRect(GetDlgItem(w,id),nullptr,FALSE); 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: case WM_KEYDOWN:
if (wp==VK_ESCAPE) { if (wp==VK_ESCAPE) {
if(!c->editing) DestroyWindow(w); // Cancel region selection only. if(!c->editing) DestroyWindow(w); // Cancel region selection only.
@@ -1009,6 +1076,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
InvalidateRect(w,nullptr,FALSE); InvalidateRect(w,nullptr,FALSE);
} }
if(!(GetKeyState(VK_CONTROL)&0x8000)) { if(!(GetKeyState(VK_CONTROL)&0x8000)) {
if(wp=='V') SendMessageW(w,WM_COMMAND,27,0);
if(wp=='T') SendMessageW(w,WM_COMMAND,26,0); if(wp=='T') SendMessageW(w,WM_COMMAND,26,0);
if(wp=='L') SendMessageW(w,WM_COMMAND,22,0); if(wp=='L') SendMessageW(w,WM_COMMAND,22,0);
if(wp=='R') SendMessageW(w,WM_COMMAND,23,0); if(wp=='R') SendMessageW(w,WM_COMMAND,23,0);
@@ -1068,6 +1136,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
InvalidateRect(w,nullptr,FALSE); return 0; InvalidateRect(w,nullptr,FALSE); return 0;
} }
c->selected=-1; c->selectedLabel=false; c->selected=-1; c->selectedLabel=false;
if(c->pointer) { InvalidateRect(w,nullptr,FALSE); updateStatus(*c); return 0; }
} }
if(c->editing && c->textTool) { if(c->editing && c->textTool) {
Mark draft=c->textDefaults; draft.text.clear(); draft.from=draft.to=p; draft.color=c->currentColor; Mark draft=c->textDefaults; draft.text.clear(); draft.from=draft.to=p; draft.color=c->currentColor;
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h" #include "platform.h"
namespace ks namespace ks
{ {
inline constexpr wchar_t AppVersion[] = L"0.6.5"; inline constexpr wchar_t AppVersion[] = L"0.6.7";
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;
struct UpdateResult struct UpdateResult
+112 -2
View File
@@ -31,16 +31,58 @@ int main()
if(info.hbmColor) DeleteObject(info.hbmColor); if(info.hbmColor) DeleteObject(info.hbmColor);
DestroyCursor(cursor); 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<std::size(ks::CursorTools);++i) if(ks::CursorTools[i]==tool) expected=c.drawingCursors[i];
require(expected && ks::actionCursor(c,{-1,ks::MarkPart::None},true)==expected,"each tool uses its own drawing cursor");
require(ks::actionCursor(c,{-1,ks::MarkPart::None},false)==LoadCursorW(nullptr,IDC_ARROW),"outside canvas uses normal pointer");
c.marks={{{20,20},{120,20},0}};
require(ks::actionCursor(c,{0,ks::MarkPart::Body},true)==LoadCursorW(nullptr,IDC_SIZEALL),"move cursor overrides drawing tool");
require(ks::actionCursor(c,{0,ks::MarkPart::Rotation},true)==LoadCursorW(nullptr,IDC_HAND),"rotation cursor overrides drawing tool");
require(ks::actionCursor(c,{0,ks::MarkPart::End},true)==LoadCursorW(nullptr,IDC_SIZEWE),"endpoint resize overrides drawing tool");
require(ks::actionCursor(c,{0,ks::MarkPart::Thickness},true)==LoadCursorW(nullptr,IDC_SIZENS),"thickness resize is perpendicular to line");
}
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)==L"KSwitcher — Screenshot #1","title includes editor index 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);
require(c.statusText.find(L"Freehand")!=std::wstring::npos,"default tool hint is shown"); require(c.statusText.find(L"Pointer")!=std::wstring::npos,"default tool hint is shown");
SendMessageW(GetDlgItem(window,13),WM_MOUSEMOVE,0,0); SendMessageW(GetDlgItem(window,13),WM_MOUSEMOVE,0,0);
require(c.statusText.find(L"Ctrl+C")!=std::wstring::npos,"button hover shows action shortcut"); require(c.statusText.find(L"Ctrl+C")!=std::wstring::npos,"button hover shows action shortcut");
SendMessageW(GetDlgItem(window,13),WM_MOUSELEAVE,0,0); SendMessageW(GetDlgItem(window,13),WM_MOUSELEAVE,0,0);
require(c.statusText.find(L"Freehand")!=std::wstring::npos,"button leave restores contextual hint"); require(c.statusText.find(L"Pointer")!=std::wstring::npos,"button leave restores contextual hint");
for(int id:ks::ToolIds) require(GetDlgItem(window,id)!=nullptr,"all toolbar buttons created"); for(int id:ks::ToolIds) require(GetDlgItem(window,id)!=nullptr,"all toolbar buttons created");
require(GetDlgItem(window,34)!=nullptr && c.toolIcons[0][14]!=nullptr,"font icon created and loaded"); require(GetDlgItem(window,34)!=nullptr && c.toolIcons[0][14]!=nullptr,"font icon created and loaded");
ks::Mark text{{20,20},{20,20},0}; text.fontSize=18; ks::Mark text{{20,20},{20,20},0}; text.fontSize=18;
@@ -144,6 +186,74 @@ int main()
auto outside=c.marks[2].to; outside.x-=4; auto outside=c.marks[2].to; outside.x-=4;
require(ks::markAt(c,outside)==std::pair{2,ks::MarkPart::End},"endpoint remains draggable outside badge"); require(ks::markAt(c,outside)==std::pair{2,ks::MarkPart::End},"endpoint remains draggable outside badge");
} }
{
auto mouse=[&](UINT message,POINT p) {
LONG x=c.view.left+MulDiv(p.x,c.view.right-c.view.left,c.image->width);
LONG y=c.view.top+MulDiv(p.y,c.view.bottom-c.view.top,c.image->height);
SendMessageW(window,message,message==WM_LBUTTONUP?0:MK_LBUTTON,MAKELPARAM(x,y));
};
SendMessageW(window,WM_COMMAND,27,0); c.marks.clear();
mouse(WM_LBUTTONDOWN,{40,160}); mouse(WM_MOUSEMOVE,{100,190}); mouse(WM_LBUTTONUP,{100,190});
require(c.marks.empty() && !c.dragging && c.selected==-1,"pointer drag on empty canvas never adds an object");
for(int tool : {11,22}) for(int target : {0,1,2}) {
if(target==0) c.marks={{{55,160},{55,160},1},{{255,160},{255,160},2}};
else {
c.marks={{{30,140},{80,190},0},{{230,140},{280,190},0}};
if(target==1) for(auto &mark:c.marks) mark.shape=ks::ScreenshotShape::Rectangle;
else for(auto &mark:c.marks) mark.from.y=mark.to.y=160;
}
SendMessageW(window,WM_COMMAND,tool,0);
BYTE savedKeys[256]{},drawingKeys[256]{};
require(GetKeyboardState(savedKeys)!=FALSE,"read thread keyboard state");
std::copy(savedKeys,savedKeys+256,drawingKeys); drawingKeys[VK_CONTROL]=0x80;
require(SetKeyboardState(drawingKeys)!=FALSE,"set thread-local Ctrl for drawing test");
mouse(WM_LBUTTONDOWN,{55,160}); mouse(WM_MOUSEMOVE,{255,160}); mouse(WM_LBUTTONUP,{255,160});
SetKeyboardState(savedKeys);
require(c.marks.size()==3,"drawing over existing objects creates a connector with Ctrl");
auto &connector=c.marks[2];
require(target==0?(connector.fromNumber==1 && connector.toNumber==2):
(connector.fromLink.target==c.marks[0].id && connector.toLink.target==c.marks[1].id),
"new arrow or line automatically attaches both endpoints to targets");
require(connector.ends==(tool==11?c.arrowEnds:ks::LineEnds::None),"arrow and straight-line tools retain their end styles");
auto before=connector.from;
SendMessageW(window,WM_KEYDOWN,'V',0);
require(c.pointer && !c.arrow && !c.freehand,"V selects pointer mode");
POINT body=target==0?POINT{55,160}:target==1?POINT{30,175}:POINT{30,160};
mouse(WM_LBUTTONDOWN,body); mouse(WM_MOUSEMOVE,{body.x,body.y-20}); mouse(WM_LBUTTONUP,{body.x,body.y-20});
require(c.selected==0 && c.marks.size()==3 && c.marks[2].from.y<before.y,
"pointer moves the target and the new connector follows without adding objects");
mouse(WM_LBUTTONDOWN,{150,220}); mouse(WM_LBUTTONUP,{150,220});
require(c.selected==-1 && c.marks.size()==3,"pointer click on empty space only clears selection");
}
}
for(bool manualAngle : {false,true}) {
c.marks={{{40,140},{180,60},0}}; c.selected=0;
c.marks[0].fontSize=18; c.marks[0].fontWeight=FW_BOLD;
c.marks[0].rotation=31; c.marks[0].labelRotationManual=manualAngle;
SendMessageW(window,WM_COMMAND,35,0);
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Sloped caption");
ks::finishInlineText(window,c,true);
auto captionAngle=ks::screenshotArrowLabel(c.marks[0]).rotation;
require(captionAngle!=0,"caption retains its automatic or manual angle");
SendMessageW(window,WM_COMMAND,26,0);
LONG x=c.view.left+MulDiv(220,c.view.right-c.view.left,c.image->width);
LONG y=c.view.top+MulDiv(190,c.view.bottom-c.view.top,c.image->height);
SendMessageW(window,WM_LBUTTONDOWN,MK_LBUTTON,MAKELPARAM(x,y));
require(c.inlineText && !c.editingLabel,"standalone text starts independently of caption");
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Independent text");
ks::finishInlineText(window,c,true);
require(c.marks.size()==2 && c.marks[1].isText() && c.marks[1].rotation==0,
"new standalone text does not inherit caption rotation");
require(c.marks[1].fontSize==18 && c.marks[1].fontWeight==FW_BOLD,
"new text still inherits font styling");
require(ks::screenshotArrowLabel(c.marks[0]).rotation==captionAngle,"new text does not change caption angle");
c.marks[1].rotation=22; c.selected=1;
SendMessageW(window,WM_KEYDOWN,VK_F2,0);
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Edited standalone text");
ks::finishInlineText(window,c,true);
require(c.marks[1].rotation==22,"editing existing rotated text preserves its own angle");
SendMessageW(window,WM_COMMAND,27,0);
}
std::vector<ks::Mark> nudgeMarks; std::vector<ks::Mark> nudgeMarks;
nudgeMarks.push_back({{60,60},{60,60},1}); nudgeMarks.push_back({{60,60},{60,60},1});
nudgeMarks.push_back({{60,60},{120,100},0}); nudgeMarks.push_back({{60,60},{120,100},0});