diff --git a/CMakeLists.txt b/CMakeLists.txt index 639d4c6..2e1a705 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.24) -project(KSwitcher VERSION 0.6.51 LANGUAGES CXX RC) +project(KSwitcher VERSION 0.6.52 LANGUAGES CXX RC) if(NOT WIN32) message(FATAL_ERROR "KSwitcher uses native Windows APIs") endif() diff --git a/README.md b/README.md index 09187c4..fdc198f 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,10 @@ snapping and shape constraints are unchanged. Both drawn and recognized triangles have three independent vertex handles instead of bounding-box resize handles. Drag any vertex to reshape the triangle while keeping the other two fixed, even after rotation. Vertex edits support undo/redo, and the rotation and thickness handles remain available. +**Arc (U)** draws an open half ellipse inside the dragged rectangle. Use the corner handles +to resize it and the round handle to rotate it (Shift snaps to 15 degrees). Arcs support +color, thickness, line style, moving, cloning, undo/redo and PNG export. + **Trapezoid (B)** on the toolbar creates a trapezoid with parallel bases. Pencil recognition also fits nearly parallel, unequal bases into a clean trapezoid. Trapezoids support bounding-box resizing, rotation, styling and undo/redo; Ctrl+Z after recognition restores the original pencil stroke. diff --git a/Release/KSwitcher-win-x64.zip b/Release/KSwitcher-win-x64.zip index 7df9883..f6f6a33 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 60ee2a2..e66335f 100644 --- a/Release/README.md +++ b/Release/README.md @@ -1,16 +1,15 @@ # Latest Windows release -## v0.6.51 — 2026-11-06 +## v0.6.52 — 2026-11-06 ## Changes -- Add Telegram Web K chat language profiles in Google Chrome. Open a chat at `https://web.telegram.org/k/`, then **Settings → Languages → Languages for a chat...** and select **Telegram Web: name**. Choose languages, then **Use languages → Save**. Applies to Alt+Shift, Win+Space, reverse Win+Shift+Space and Next layout. -- Read the trusted Telegram address and chat header through Chrome accessibility. Refresh on foreground/focus and address/header events, without OCR, a polling timer or a browser extension. Web profiles are separate from Telegram Desktop profiles; identical chat names across accounts/browser profiles share a web profile. Web A and other browsers are not advertised as supported. Telegram Desktop and Teams remain unchanged; Viber OCR remains disabled. -- Add application-specific snippets. In **Tools → Snippets**, enable **Only in these applications**, enter comma-separated executable names or select an open application and choose **Add app**. Unchecked means everywhere. Matching ignores case and the optional `.exe` suffix. Chrome scope applies to the whole browser. -- Prefer an application-specific snippet over a global snippet with the same trigger. Allow the same trigger in disjoint application scopes and reject overlapping scopes. Keep existing snippets global. Preserve scopes in manual import/export and synchronization, with scope-aware merge conflicts. Validate the target application before insertion. -- Center **Add app** against its application selector. +- Add **Arc (U)** to the screenshot editor toolbar, with its own cursor and drawing hint. Drag a rectangle to draw an open half ellipse; the endpoints are not joined by a straight chord. +- Resize arcs using the corner handles, rotate them with the round handle (Shift snaps to 15 degrees), move or clone them, and adjust their color, thickness and line style. Undo/redo, connector attachment and full-resolution PNG export preserve the open curve. +- Widen the editor's minimum window width to fit the additional toolbar button. +- Retain v0.6.51 Telegram Web K chat profiles in Chrome and application-specific snippets, including their compatibility requirements below. ## Validation -Passed 674 chat-profile/snippet UI checks, 568 input checks (including app-specific precedence and global fallback), 108 sync checks, 332 native checks, complete dictionary equivalence (140,939 words), wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress checks. Inspected the snippet editor at 250% scaling. Live Chrome checks read a Telegram chat name directly and observed cache activation/invalidation. Release binaries, package manifest, published assets and update selection are checked during publication. +Passed 332 native checks, 568 input checks, 674 chat-profile checks, 108 sync checks, dictionary equivalence (140,939 words), wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress checks. Arc checks cover toolbar selection, drawing, open-curve hit testing and rendering, resizing, rotation, connector attachment, undo/redo and PNG export. Inspected an actual editor rendering with the Arc tool selected. Published package checksums, manifests, executable versions, update selection and English/Ukrainian Wiki pages are verified during publication. ## Upgrade and compatibility 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 d72cdb1..610391a 100644 --- a/Release/SHA256SUMS.txt +++ b/Release/SHA256SUMS.txt @@ -1 +1 @@ -0ee6426bf1715ee3156a1f8dbb2d213a9e09a49b2fcfec553d6049dc6a868877 KSwitcher-win-x64.zip +fdc093c4a45c55fcee7ae4c9b597d56ea95293bd8995ed84cc2c0460e8855d58 KSwitcher-win-x64.zip diff --git a/resources/app.rc b/resources/app.rc index b426bed..170cf18 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,51,0 - PRODUCTVERSION 0,6,51,0 + FILEVERSION 0,6,52,0 + PRODUCTVERSION 0,6,52,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.51" + VALUE "FileVersion", "0.6.52" END END BLOCK "VarFileInfo" diff --git a/src/screenshot.cpp b/src/screenshot.cpp index 466a0f2..eba07d9 100644 --- a/src/screenshot.cpp +++ b/src/screenshot.cpp @@ -54,7 +54,7 @@ bool confirmScreenshotClose(HWND owner) // If Windows cannot display the confirmation, keep the screenshot open. return SUCCEEDED(result) && button==IDYES; } -constexpr int ToolIds[]{27,10,11,22,23,24,25,36,37,21,19,26,20,12,18,13}; +constexpr int ToolIds[]{27,10,11,22,23,24,25,36,37,38,21,19,26,20,12,18,13}; struct Surface { HDC dc = CreateCompatibleDC(nullptr); @@ -124,7 +124,7 @@ 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,36,37,19}; +constexpr int CursorTools[]{10,11,22,23,24,25,36,37,38,19}; HCURSOR makeToolCursor(int tool,int size) { // Monochrome AND/XOR cursor: white outline keeps the glyph visible on dark images. @@ -148,6 +148,7 @@ HCURSOR makeToolCursor(int tool,int size) 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 38: Arc(dc,x(16),y(18),x(31),y(38),x(31),y(28),x(16),y(28)); break; case 25: Ellipse(dc,x(16),y(20),x(31),y(28)); break; case 36: { POINT p[]{{x(23),y(17)},{x(31),y(30)},{x(16),y(30)},{x(23),y(17)}}; Polyline(dc,p,4); break; } case 37: { POINT p[]{{x(20),y(18)},{x(27),y(18)},{x(31),y(30)},{x(16),y(30)},{x(20),y(18)}}; Polyline(dc,p,5); break; } @@ -268,6 +269,7 @@ void updateStatus(Capture &c) 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 = paint over an object | V = pointer"; else if(c.freehand) text=L"Freehand (F): auto-smooth / shapes; Ctrl+Z restores original | Shift = 45-degree steps | Ctrl = draw over objects"; + else if(c.shape==ScreenshotShape::Arc) text=L"Arc (U): drag to draw a half ellipse | Handles = resize / rotate | V = pointer"; 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 | V = select and edit | Ctrl+C = copy screenshot"; text=L"Zoom: "+std::to_wstring(int(std::lround(100.0*(c.view.right-c.view.left)/c.image->width)))+ @@ -409,6 +411,14 @@ std::vector shapeOutline(const Mark &m) auto r=region(m.from,m.to); if(m.shape==ScreenshotShape::Rectangle) points={{r.left,r.top},{r.right,r.top},{r.right,r.bottom},{r.left,r.bottom},{r.left,r.top}}; + else if(m.shape==ScreenshotShape::Arc) { + // An open half ellipse spanning the drag rectangle, with no closing chord. + for(int i=0;i<=90;++i) { + double a=i*3.141592653589793/90; + points.push_back({LONG(std::lround((r.left+r.right)/2.0-(r.right-r.left)/2.0*std::cos(a))), + LONG(std::lround(r.bottom-(r.bottom-r.top)*std::sin(a)))}); + } + } else if(m.shape==ScreenshotShape::Triangle) points={{(r.left+r.right)/2,r.top},{r.right,r.bottom},{r.left,r.bottom},{(r.left+r.right)/2,r.top}}; else if(m.shape==ScreenshotShape::Trapezoid) @@ -550,7 +560,7 @@ void drawMarks(HDC dc, const std::vector &marks, const Mark *preview = nul if(m.shape!=ScreenshotShape::None) { auto hollow=SelectObject(dc,GetStockObject(HOLLOW_BRUSH)); auto r=region(m.from,m.to); - if(!m.points.empty() || m.rotation!=0 || m.shape==ScreenshotShape::Triangle || m.shape==ScreenshotShape::Trapezoid) { + if(!m.points.empty() || m.rotation!=0 || m.shape==ScreenshotShape::Triangle || m.shape==ScreenshotShape::Trapezoid || m.shape==ScreenshotShape::Arc) { auto points=shapeOutline(m); Polyline(dc,points.data(),int(points.size())); } else if(m.shape==ScreenshotShape::Rectangle) Rectangle(dc,r.left,r.top,r.right+1,r.bottom+1); @@ -668,7 +678,7 @@ void layout(HWND w, Capture &c) RECT statusRect{}; GetWindowRect(status,&statusRect); c.statusHeight=statusRect.bottom-statusRect.top; r.bottom-=c.statusHeight; - const int positions[]{8,44,80,116,152,188,224,260,296,332,368,404,440,488,524,572}; + const int positions[]{8,44,80,116,152,188,224,260,296,332,368,404,440,476,524,560,608}; static_assert(std::size(positions)==std::size(ToolIds)); for (size_t i = 0; i < std::size(ToolIds); ++i) { @@ -677,7 +687,7 @@ void layout(HWND w, Capture &c) MulDiv(32,dpi,96),MulDiv(32,dpi,96),TRUE); } SendDlgItemMessageW(w,28,WM_SETFONT,(WPARAM)c.font,TRUE); - MoveWindow(GetDlgItem(w,28),MulDiv(612,dpi,96),MulDiv(6,dpi,96), + MoveWindow(GetDlgItem(w,28),MulDiv(648,dpi,96),MulDiv(6,dpi,96), MulDiv(78,dpi,96),MulDiv(32,dpi,96),TRUE); c.panelHeight=c.unit+MulDiv(40,dpi,96); for(int id:{30,31,32,33,34,35}) SendDlgItemMessageW(w,id,WM_SETFONT,(WPARAM)c.font,TRUE); @@ -771,7 +781,7 @@ 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::Trapezoid?37:c.shape==ScreenshotShape::Triangle?36:c.shape!=ScreenshotShape::None?22+int(c.shape):c.arrow?11:10; + int tool=c.highlight?19:c.straight?22:c.shape==ScreenshotShape::Arc?38:c.shape==ScreenshotShape::Trapezoid?37:c.shape==ScreenshotShape::Triangle?36:c.shape!=ScreenshotShape::None?22+int(c.shape):c.arrow?11:10; for(size_t i=0;i=23 && item.CtlID<=25 && int(c.shape)==int(item.CtlID)-22)||(item.CtlID==26 && c.textTool)|| - (item.CtlID==36 && c.shape==ScreenshotShape::Triangle)||(item.CtlID==37 && c.shape==ScreenshotShape::Trapezoid); + (item.CtlID==36 && c.shape==ScreenshotShape::Triangle)||(item.CtlID==37 && c.shape==ScreenshotShape::Trapezoid)||(item.CtlID==38 && c.shape==ScreenshotShape::Arc); bool disabled=item.itemState&ODS_DISABLED; POINT mouse; GetCursorPos(&mouse); ScreenToClient(item.hwndItem,&mouse); bool hover=PtInRect(&r,mouse) && !disabled; @@ -897,13 +907,14 @@ void drawTool(const DRAWITEMSTRUCT &item, const Capture &c) RoundRect(dc,r.left,r.top,r.right,r.bottom,corner,corner); 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; - auto icon=(item.CtlID==36 || item.CtlID==37)?nullptr:item.CtlID==27?(HICON)LoadCursorW(nullptr,IDC_ARROW):c.toolIcons[disabled?2:active?1:0][iconIndex]; - if(item.CtlID==36 || item.CtlID==37) { + auto icon=(item.CtlID==36 || item.CtlID==37 || item.CtlID==38)?nullptr:item.CtlID==27?(HICON)LoadCursorW(nullptr,IDC_ARROW):c.toolIcons[disabled?2:active?1:0][iconIndex]; + if(item.CtlID==36 || item.CtlID==37 || item.CtlID==38) { int cx=(r.left+r.right)/2,cy=(r.top+r.bottom)/2,s=c.iconSize/2; auto trianglePen=CreatePen(PS_SOLID,std::max(1,MulDiv(2,GetDpiForWindow(item.hwndItem),96)), disabled?RGB(148,163,184):active?RGB(37,99,235):RGB(51,65,85)); auto previous=SelectObject(dc,trianglePen); - if(item.CtlID==36) { POINT vertices[]{{cx,cy-s},{cx+s,cy+s},{cx-s,cy+s},{cx,cy-s}}; Polyline(dc,vertices,4); } + if(item.CtlID==38) Arc(dc,cx-s,cy-s,cx+s,cy+3*s,cx+s,cy+s,cx-s,cy+s); + else if(item.CtlID==36) { POINT vertices[]{{cx,cy-s},{cx+s,cy+s},{cx-s,cy+s},{cx,cy-s}}; Polyline(dc,vertices,4); } else { POINT vertices[]{{cx-s/2,cy-s},{cx+s/2,cy-s},{cx+s,cy+s},{cx-s,cy+s},{cx-s/2,cy-s}}; Polyline(dc,vertices,5); } SelectObject(dc,previous); DeleteObject(trianglePen); } @@ -1025,7 +1036,7 @@ void edit(HWND w, Capture &c) InitCommonControlsEx(&statusControls); CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS, 0,0,0,0,w,(HMENU)50,c.app.instance,nullptr); - 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"Triangle (G): drag to draw; drag each vertex to reshape", L"Trapezoid (B): drag to draw", L"Freehand line (F); Shift for 45-degree steps; move >50 px off line to turn; Ctrl to draw over objects", 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"Triangle (G): drag to draw; drag each vertex to reshape", L"Trapezoid (B): drag to draw", L"Arc (U): drag to draw a half ellipse; use handles to resize or rotate", L"Freehand line (F); Shift for 45-degree steps; move >50 px off line to turn; Ctrl to draw over objects", 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)"}; static_assert(std::size(labels)==std::size(ToolIds)); auto saveButton=CreateWindowW(L"BUTTON",L"Save...",WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON, @@ -1120,7 +1131,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) } break; case WM_DRAWITEM: - if((wp>=10 && wp<=13) || (wp>=18 && wp<=27) || wp==34 || wp==35 || wp==36 || wp==37) { drawTool(*reinterpret_cast(lp),*c); return TRUE; } + if((wp>=10 && wp<=13) || (wp>=18 && wp<=27) || wp==34 || wp==35 || wp==36 || wp==37 || wp==38) { drawTool(*reinterpret_cast(lp),*c); return TRUE; } break; case WM_CTLCOLORSTATIC: SetBkColor((HDC)wp,RGB(255,255,255)); SetTextColor((HDC)wp,RGB(51,65,85)); @@ -1214,13 +1225,13 @@ 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; } c->syncHistory(); } - if (LOWORD(wp)==10 || LOWORD(wp)==11 || LOWORD(wp)==19 || LOWORD(wp)==36 || LOWORD(wp)==37 || (LOWORD(wp)>=21 && LOWORD(wp)<=27)) + if (LOWORD(wp)==10 || LOWORD(wp)==11 || LOWORD(wp)==19 || LOWORD(wp)==36 || LOWORD(wp)==37 || LOWORD(wp)==38 || (LOWORD(wp)>=21 && LOWORD(wp)<=27)) { c->pointer=LOWORD(wp)==27; - c->arrow = LOWORD(wp)==11 || LOWORD(wp)==36 || LOWORD(wp)==37 || (LOWORD(wp)>=22 && LOWORD(wp)<=25); + c->arrow = LOWORD(wp)==11 || LOWORD(wp)==36 || LOWORD(wp)==37 || LOWORD(wp)==38 || (LOWORD(wp)>=22 && LOWORD(wp)<=25); c->straight=LOWORD(wp)==22; c->textTool=LOWORD(wp)==26; - c->shape=LOWORD(wp)==37?ScreenshotShape::Trapezoid:LOWORD(wp)==36?ScreenshotShape::Triangle:LOWORD(wp)>=23 && LOWORD(wp)<=25?ScreenshotShape(LOWORD(wp)-22):ScreenshotShape::None; + c->shape=LOWORD(wp)==38?ScreenshotShape::Arc:LOWORD(wp)==37?ScreenshotShape::Trapezoid:LOWORD(wp)==36?ScreenshotShape::Triangle:LOWORD(wp)>=23 && LOWORD(wp)<=25?ScreenshotShape(LOWORD(wp)-22):ScreenshotShape::None; c->selected=-1; c->selectedLabel=false; c->highlight=LOWORD(wp)==19; c->freehand=LOWORD(wp)==21; @@ -1233,6 +1244,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); InvalidateRect(GetDlgItem(w,36),nullptr,FALSE); InvalidateRect(GetDlgItem(w,37),nullptr,FALSE); + InvalidateRect(GetDlgItem(w,38),nullptr,FALSE); } SetFocus(w); captureCursor(w,*c); InvalidateRect(w,nullptr,FALSE); return 0; @@ -1293,6 +1305,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) if(wp=='E') SendMessageW(w,WM_COMMAND,25,0); if(wp=='G') SendMessageW(w,WM_COMMAND,36,0); if(wp=='B') SendMessageW(w,WM_COMMAND,37,0); + if(wp=='U') SendMessageW(w,WM_COMMAND,38,0); } if (wp=='N' || wp=='A') SendMessageW(w,WM_COMMAND,wp=='N'?10:11,0); if(wp=='H') SendMessageW(w,WM_COMMAND,19,0); @@ -1563,7 +1576,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) c->marks.back().style=c->lineStyle; c->marks.back().ends=c->straight || c->shape!=ScreenshotShape::None?LineEnds::None:c->arrowEnds; c->marks.back().shape=c->shape; auto &mark=c->marks.back(); - if(mark.shape==ScreenshotShape::Triangle || mark.shape==ScreenshotShape::Trapezoid) mark.points=shapeOutline(mark); + if(mark.shape==ScreenshotShape::Triangle || mark.shape==ScreenshotShape::Trapezoid || mark.shape==ScreenshotShape::Arc) mark.points=shapeOutline(mark); linkScreenshotEndpoint(mark,true,c->start,c->marks,6); linkScreenshotEndpoint(mark,false,c->end,c->marks,6); resolveScreenshotLinks(c->marks); @@ -1586,7 +1599,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) if (c->editing) { auto limits = reinterpret_cast(lp); - limits->ptMinTrackSize = {MulDiv(730,GetDpiForWindow(w),96),MulDiv(240,GetDpiForWindow(w),96)}; + limits->ptMinTrackSize = {MulDiv(766,GetDpiForWindow(w),96),MulDiv(240,GetDpiForWindow(w),96)}; return 0; } break; diff --git a/src/screenshot.h b/src/screenshot.h index 1e501af..601e9df 100644 --- a/src/screenshot.h +++ b/src/screenshot.h @@ -15,7 +15,7 @@ void showScreenshot(ScreenshotHost &); void showScreenshotEditor(ScreenshotHost &,const Clip &); enum class LineStyle { Solid, Dotted, Dashed, DashDot }; enum class LineEnds { End, Start, Both, None }; -enum class ScreenshotShape { None, Rectangle, Circle, Ellipse, Triangle, Trapezoid }; +enum class ScreenshotShape { None, Rectangle, Circle, Ellipse, Triangle, Trapezoid, Arc }; struct ScreenshotOffset { double x, y; }; struct ScreenshotLink { unsigned long long target{}; diff --git a/src/update.h b/src/update.h index d668409..8332353 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.51"; +inline constexpr wchar_t AppVersion[] = L"0.6.52"; inline constexpr UINT UpdateMessage = WM_APP + 7; inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000; inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000; diff --git a/tests/screenshot_ui.cpp b/tests/screenshot_ui.cpp index cb4f4aa..4c53a10 100644 --- a/tests/screenshot_ui.cpp +++ b/tests/screenshot_ui.cpp @@ -141,6 +141,38 @@ int main(int argc, char **argv) SendMessageW(window,WM_COMMAND,12,0); require(c.marks.empty(),"undo trapezoid creation"); SendMessageW(window,WM_COMMAND,18,0); require(c.marks[0].shape==ks::ScreenshotShape::Trapezoid,"redo trapezoid creation"); c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{}; + require(GetDlgItem(window,38)!=nullptr,"arc has a dedicated toolbar button"); + SendMessageW(window,WM_KEYDOWN,'U',0); + require(c.shape==ks::ScreenshotShape::Arc && c.arrow && !c.freehand,"U selects arc tool"); + mouse(WM_LBUTTONDOWN,{60,70}); mouse(WM_LBUTTONUP,{240,180}); + require(c.marks.size()==1 && c.marks[0].shape==ks::ScreenshotShape::Arc && !c.marks[0].isHighlight(), + "arc tool creates an editable shape"); + auto arc=c.marks[0]; + require(ks::hitScreenshotMark(arc,{150,70},3)==ks::MarkPart::Body && + ks::hitScreenshotMark(arc,{150,180},3)==ks::MarkPart::None, + "arc selects its curve but has no closing chord"); + ks::Surface rendered(320,240); PatBlt(rendered.dc,0,0,320,240,WHITENESS); + ks::drawMarks(rendered.dc,{arc}); + require(GetPixel(rendered.dc,150,70)==arc.color && GetPixel(rendered.dc,150,180)==RGB(255,255,255), + "arc renders an open curve without a closing chord"); + auto exported=ks::decodeImage(ks::screenshotClip(c.image->dc,320,240,{arc})); + auto channel=[&](int x,int y,int component){return exported.bgra[(y*exported.width+x)*4+component];}; + require(channel(150,70,2)==GetRValue(arc.color) && channel(150,70,1)==GetGValue(arc.color) && + channel(150,180,0)==255 && channel(150,180,1)==255 && channel(150,180,2)==255, + "PNG export preserves the arc and its open chord"); + mouse(WM_LBUTTONDOWN,arc.to); mouse(WM_LBUTTONUP,{260,200}); + require(c.marks[0].to.x==260 && c.marks[0].points.back().y==200,"arc resizes with its bounds"); + auto arcGrip=ks::screenshotShapeRotationHandle(arc); + auto turned=ks::dragScreenshotMark(arc,ks::MarkPart::Rotation,arcGrip,{280,135},320,240); + require(turned.rotation!=0 && ks::hitScreenshotMark(turned,ks::rotateShapePoint(turned,{150,70}),3)==ks::MarkPart::Body, + "rotated arc remains selectable"); + std::vector linked{arc}; ks::Mark connector{{10,10},{150,70},0}; + ks::linkScreenshotEndpoint(connector,false,{150,70},linked,4); + require(connector.toLink.target!=0,"connector can attach to the arc curve"); + SendMessageW(window,WM_COMMAND,12,0); require(c.marks[0].to.x==240,"undo arc resize"); + SendMessageW(window,WM_COMMAND,12,0); require(c.marks.empty(),"undo arc creation"); + SendMessageW(window,WM_COMMAND,18,0); require(c.marks[0].shape==ks::ScreenshotShape::Arc,"redo arc creation"); + c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{}; SendMessageW(window,WM_COMMAND,21,0); } {