// Exercise the actual editor controls without capturing the desktop or changing the clipboard. // Including the implementation lets this integration test drive private editor state directly. #include "../src/screenshot.cpp" #include int main(int argc, char **argv) { CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED); try { if(argc>1 && std::string(argv[1])=="--save-only") { auto require=[](bool ok,const char *message){if(!ok) throw std::runtime_error(message);}; auto root=std::filesystem::temp_directory_path()/(L"KSwitcher-save-"+std::to_wstring(GetCurrentProcessId())); std::filesystem::create_directories(root); struct Cleanup {std::filesystem::path root; ~Cleanup(){std::error_code ec; std::filesystem::remove_all(root,ec);}} cleanup{root}; ks::ScreenshotHost app{GetModuleHandleW(nullptr)}; ks::Capture c(app); c.image=std::make_unique(320,240); PatBlt(c.image->dc,0,0,320,240,WHITENESS); c.marks={ks::Mark{{40,40},{180,140},0}}; auto expected=ks::decodeImage(ks::screenshotClip(c.image->dc,320,240,c.marks)); for(size_t i=3;iBOOL { if(!(dialog->Flags&OFN_OVERWRITEPROMPT) || std::wstring(dialog->lpstrDefExt)!=L"png" || !std::wstring(dialog->lpstrFile).ends_with(L"Знімок.png")) throw std::runtime_error("Save As dialog must preserve filename, PNG extension and overwrite confirmation"); return FALSE; }); require(!c.copied && c.savedPath==path,"cancelled Save As preserves dirty state and remembered path"); auto another=root/L"Another.png"; ks::save(nullptr,c,true,[](LPOPENFILENAMEW dialog)->BOOL { auto destination=std::filesystem::path(dialog->lpstrFile).parent_path()/L"Another.png"; wcscpy_s(dialog->lpstrFile,dialog->nMaxFile,destination.c_str()); return TRUE; }); require(c.savedPath==another && std::filesystem::exists(path),"new destination preserves previous file"); c.copied=false; bool failed=false; try {ks::saveToFile(nullptr,c,root/L"missing"/L"fail.png");} catch(const std::exception &){failed=true;} require(failed && !c.copied && c.savedPath==another,"failed save preserves dirty state and previous destination"); require(std::distance(std::filesystem::directory_iterator(root),std::filesystem::directory_iterator{})==2, "saving leaves no temporary files"); bool invalid=false; try {ks::save(nullptr,c,true,[](LPOPENFILENAMEW dialog)->BOOL { auto destination=std::filesystem::path(dialog->lpstrFile).parent_path()/L"Wrong.jpg"; wcscpy_s(dialog->lpstrFile,dialog->nMaxFile,destination.c_str()); return TRUE; });} catch(const std::exception &){invalid=true;} require(invalid && !std::filesystem::exists(root/L"Wrong.jpg"),"PNG export rejects a misleading file extension"); CoUninitialize(); std::cout<<"Passed screenshot PNG save regressions without opening windows\n"; return 0; } INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES}; InitCommonControlsEx(&controls); ks::ScreenshotHost app{GetModuleHandleW(nullptr)}; app.window=CreateWindowW(L"STATIC",L"",0,0,0,0,0,HWND_MESSAGE,nullptr,app.instance,nullptr); ks::Capture c(app); c.image=std::make_unique(320,240); PatBlt(c.image->dc,0,0,320,240,WHITENESS); c.start={0,0}; c.end={320,240}; WNDCLASSW cls{}; cls.hInstance=app.instance; cls.lpfnWndProc=ks::captureProc; cls.lpszClassName=L"KSwitcher.Editor.Integration"; RegisterClassW(&cls); auto window=CreateWindowW(cls.lpszClassName,L"Editor test",WS_POPUP,0,0,320,240,app.window,nullptr,app.instance,&c); auto require=[](bool ok,const char *message){ if(!ok) throw std::runtime_error(message); }; require(window!=nullptr,"editor creation"); ks::edit(window,c); ShowWindow(window,SW_HIDE); require(GetWindow(window,GW_OWNER)==nullptr && (GetWindowLongPtrW(window,GWL_EXSTYLE)&WS_EX_APPWINDOW), "editor is an independent Alt+Tab application window"); require(GetDlgItem(window,50)!=nullptr && c.statusHeight>0,"status bar is created"); require(c.pencilCursor!=nullptr,"pencil drawing cursor loads"); require(c.freehand && !c.pointer && ks::toolCursor(c)==c.pencilCursor, "new screenshot editor starts with the pencil tool"); { BYTE savedKeys[256]{},keys[256]{}; GetKeyboardState(savedKeys); std::copy(savedKeys,savedKeys+256,keys); keys[VK_SHIFT]=0; SetKeyboardState(keys); auto mouse=[&](UINT message,POINT p,bool shift=false) { int x=c.view.left+MulDiv(p.x,c.view.right-c.view.left,c.image->width); int 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)|(shift?MK_SHIFT:0),MAKELPARAM(x,y)); }; mouse(WM_LBUTTONDOWN,{40,40},true); mouse(WM_MOUSEMOVE,{100,50},true); require(c.stroke.to.x==100 && c.stroke.to.y==40,"Shift constrains pencil horizontally"); mouse(WM_MOUSEMOVE,{100,90},true); require(c.stroke.to.x==100 && c.stroke.to.y==40 && c.stroke.points.size()==2, "50 pixel deviation keeps the current axis"); mouse(WM_MOUSEMOVE,{100,91},true); require(c.stroke.to.x==100 && c.stroke.to.y==91 && c.stroke.points.size()==3 && c.stroke.points[1].y==40, "51 pixel deviation turns vertically and preserves the horizontal segment"); mouse(WM_MOUSEMOVE,{70,140}); require(c.stroke.to.x==70 && c.stroke.to.y==140 && c.stroke.points.size()==4, "releasing Shift resumes freehand in the same stroke"); SendMessageW(window,WM_KEYDOWN,VK_SHIFT,0); mouse(WM_MOUSEMOVE,{140,150},true); require(c.stroke.to.x==140 && c.stroke.to.y==140 && c.stroke.points[3].x==70, "pressing Shift mid-stroke anchors at the last drawn point"); SendMessageW(window,WM_KEYUP,VK_SHIFT,0); SendMessageW(window,WM_KEYDOWN,VK_SHIFT,0); mouse(WM_MOUSEMOVE,{150,200},true); mouse(WM_LBUTTONUP,{150,210},true); SetKeyboardState(savedKeys); require(c.marks.size()==1 && c.marks[0].to.x==140 && c.marks[0].to.y==210, "repressing Shift without mouse movement starts a new segment and constrains mouse-up"); SendMessageW(window,WM_COMMAND,12,0); require(c.marks.empty(),"mixed freehand and constrained segments undo as one stroke"); SendMessageW(window,WM_COMMAND,18,0); require(c.marks.size()==1 && c.marks[0].points.size()==6,"redo preserves constrained stroke geometry"); c.marks.clear(); c.selected=-1; for(int sx:{-1,1}) for(int sy:{-1,1}) { mouse(WM_LBUTTONDOWN,{160,120},true); mouse(WM_MOUSEMOVE,{160+sx,120},true); for(int step=2;step<=90;++step) mouse(WM_MOUSEMOVE,{160+sx*step,120+sy*step},true); require(c.stroke.points.size()==2 && c.stroke.to.x==160+sx*90 && c.stroke.to.y==120+sy*90, "smooth diagonal after one-pixel horizontal jitter stays diagonal without stairs"); mouse(WM_LBUTTONUP,{160+sx*90,120+sy*90},true); c.marks.clear(); c.selected=-1; mouse(WM_LBUTTONDOWN,{160-sx*40,120-sy*70},true); mouse(WM_MOUSEMOVE,{160,120-sy*70},true); for(int step=1;step<=90;++step) mouse(WM_MOUSEMOVE,{160+sx*step,120-sy*70+sy*step},true); require(c.stroke.points.size()==3 && c.strokeAxis>=3 && std::abs(c.stroke.to.x-c.stroke.points[1].x)==std::abs(c.stroke.to.y-c.stroke.points[1].y), "smooth turn from horizontal into diagonal uses raw mouse direction without stairs"); mouse(WM_LBUTTONUP,{160+sx*90,120-sy*70+sy*90},true); c.marks.clear(); c.selected=-1; } for(int sx:{-1,1}) for(int sy:{-1,1}) { mouse(WM_LBUTTONDOWN,{160,120},true); mouse(WM_MOUSEMOVE,{160+sx*60,120+sy*50},true); require(c.stroke.to.x==160+sx*55 && c.stroke.to.y==120+sy*55, "Shift snaps to exact 45 degree diagonals in all four directions"); mouse(WM_LBUTTONUP,{160+sx*60,120+sy*50},true); c.marks.clear(); c.selected=-1; } mouse(WM_LBUTTONDOWN,{40,40},true); mouse(WM_MOUSEMOVE,{100,40},true); mouse(WM_MOUSEMOVE,{160,100},true); require(c.stroke.points.size()==3 && c.stroke.to.x==160 && c.stroke.to.y==100 && c.stroke.points[1].x==100 && c.stroke.points[1].y==40, "large deviation turns a horizontal segment diagonally without losing its corner"); mouse(WM_MOUSEMOVE,{200,100},true); require(c.stroke.to.x==180 && c.stroke.to.y==120 && c.stroke.points.size()==3, "small perpendicular deviation keeps the diagonal axis"); mouse(WM_MOUSEMOVE,{260,120},true); require(c.stroke.to.x==260 && c.stroke.to.y==120 && c.stroke.points.size()==4, "diagonal deviation beyond 50 pixels turns back to horizontal"); mouse(WM_LBUTTONUP,{260,120},true); c.marks.clear(); c.selected=-1; for(int sx:{-1,1}) for(int sy:{-1,1}) { POINT start{sx>0?310L:9L,120}; mouse(WM_LBUTTONDOWN,start,true); mouse(WM_MOUSEMOVE,{start.x+sx*8,start.y+sy*8},true); mouse(WM_MOUSEMOVE,{sx>0?319L:0L,start.y+sy*25},true); auto end=c.stroke.to; require(end.x>=0 && end.x<320 && end.y>=0 && end.y<240 && std::abs(end.x-start.x)==std::abs(end.y-start.y), "diagonal stays at 45 degrees and inside image boundaries"); mouse(WM_LBUTTONUP,{sx>0?319L:0L,start.y+sy*25},true); c.marks.clear(); c.selected=-1; } keys[VK_CONTROL]=0x80; SetKeyboardState(keys); mouse(WM_LBUTTONDOWN,{40,40}); mouse(WM_MOUSEMOVE,{100,70}); mouse(WM_LBUTTONUP,{120,90}); SetKeyboardState(savedKeys); require(c.marks.size()==1 && c.marks[0].points[1].x==100 && c.marks[0].points[1].y==70 && c.marks[0].to.x==120 && c.marks[0].to.y==90, "Ctrl keeps its existing drawing behavior without constraining the pencil"); c.marks.clear(); c.selected=-1; } for(int size:{32,48,64,96}) { auto cursor=(HCURSOR)LoadImageW(app.instance,MAKEINTRESOURCEW(IDC_SCREENSHOT_PENCIL),IMAGE_CURSOR,size,size,0); ICONINFO info{}; require(cursor && GetIconInfo(cursor,&info) && !info.fIcon,"pencil cursor supports standard and high DPI"); require(info.xHotspotUINT(size*2/3),"pencil cursor draws from its tip"); if(info.hbmMask) DeleteObject(info.hbmMask); 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;iwidth/(c.view.right-c.view.left)); c.selected=0; require(ks::markAt(c,captionGrip).second==ks::MarkPart::Rotation,"caption rotation handle is interactive"); c.marks[0]=ks::dragScreenshotMark(c.marks[0],ks::MarkPart::Rotation,captionGrip,{captionGrip.x+40,captionGrip.y+20},320,240,true); auto angle=c.marks[0].rotation; SendMessageW(window,WM_KEYDOWN,VK_F2,0); SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Rotated caption"); ks::finishInlineText(window,c,true); require(c.marks[0].rotation==angle && c.marks[0].label==L"Rotated caption","editing caption preserves its rotation"); auto rotating=text; rotating.text=L"Rotate"; c.marks.push_back(rotating); c.selected=1; auto handle=ks::screenshotTextRotationHandle(rotating,double(c.unit*0.6)*c.image->width/(c.view.right-c.view.left)); require(ks::markAt(c,handle).second==ks::MarkPart::Rotation,"rotation handle is interactive"); c.hoveredPart=ks::MarkPart::Rotation; ks::updateStatus(c); require(c.statusText.find(L"15-degree")!=std::wstring::npos,"rotation hint explains Shift snapping"); c.hoveredPart=ks::MarkPart::None; // Drag the two badges in turn, including a click near the attached endpoint. for(bool freehand : {false,true}) for(auto ends : {ks::LineEnds::End,ks::LineEnds::None}) { c.marks={{{60,80},{60,80},1},{{230,80},{230,80},2},{{60,80},{230,80},0}}; auto &connector=c.marks[2]; connector.fromNumber=1; connector.toNumber=2; connector.ends=ends; connector.freehand=freehand; if(freehand) connector.points={{60,80},{140,80},{230,80}}; ks::resolveScreenshotLinks(c.marks); c.selected=-1; c.selectedLabel=false; 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)); }; for(int badge : {0,1,0,1}) { auto origin=c.marks[badge].from; // The second badge's inner edge overlaps the connector endpoint hit area. if(badge==1) origin.x-=12; mouse(WM_LBUTTONDOWN,origin); require(c.selected==badge && c.part==ks::MarkPart::Body,"click on linked badge selects badge instead of connector endpoint"); mouse(WM_MOUSEMOVE,{origin.x,origin.y+20}); mouse(WM_LBUTTONUP,{origin.x,origin.y+20}); require(c.marks[2].fromNumber==1 && c.marks[2].toNumber==2,"alternating badge drags preserve both connector links"); auto start=c.marks[0].from,end=c.marks[1].from; require(std::abs(std::hypot(double(c.marks[2].from.x-start.x),double(c.marks[2].from.y-start.y))-15)<1 && std::abs(std::hypot(double(c.marks[2].to.x-end.x),double(c.marks[2].to.y-end.y))-15)<1, "connector endpoints follow both badge rims"); } // Selecting a linked connector (also the first click of a double-click) // must not detach it before opening its caption editor. for(int entry : {0,1,2}) { c.selected=-1; POINT body{110,c.marks[0].from.y}; if(freehand) body={(c.marks[2].points[0].x+c.marks[2].points[1].x)/2, (c.marks[2].points[0].y+c.marks[2].points[1].y)/2}; mouse(WM_LBUTTONDOWN,body); mouse(WM_LBUTTONUP,body); require(c.selected==2 && c.marks[2].fromNumber==1 && c.marks[2].toNumber==2, "clicking a connector without moving preserves both links"); if(entry==0) { mouse(WM_LBUTTONDBLCLK,body); mouse(WM_LBUTTONUP,body); } else if(entry==1) SendMessageW(window,WM_KEYDOWN,VK_F2,0); else SendMessageW(window,WM_COMMAND,35,0); require(c.inlineText && c.editingLabel,"linked connector caption opens through every entry point"); SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Linked caption"); ks::finishInlineText(window,c,true); require(c.marks[2].label==L"Linked caption" && c.marks[2].fromNumber==1 && c.marks[2].toNumber==2, "adding or editing a caption preserves both connector links"); } c.selected=2; require(ks::markAt(c,c.marks[1].from).first==1,"selected connector cannot steal a badge click"); 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"); } { 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.ywidth); 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 nudgeMarks; nudgeMarks.push_back({{60,60},{60,60},1}); nudgeMarks.push_back({{60,60},{120,100},0}); auto nudgeText=text; nudgeText.text=L"Move"; nudgeText.from=nudgeText.to={60,60}; nudgeMarks.push_back(nudgeText); for(auto shape:{ks::ScreenshotShape::Rectangle,ks::ScreenshotShape::Circle,ks::ScreenshotShape::Ellipse}) { ks::Mark mark{{60,60},{100,100},0}; mark.shape=shape; nudgeMarks.push_back(mark); } for(bool freehand:{false,true}) { ks::Mark mark{{60,60},{120,100},0}; mark.points={{60,60},{90,80},{120,100}}; mark.freehand=freehand; nudgeMarks.push_back(mark); } for(auto mark:nudgeMarks) { c.marks={mark}; c.selected=0; c.selectedLabel=false; SendMessageW(window,WM_KEYDOWN,VK_RIGHT,0); SendMessageW(window,WM_KEYDOWN,VK_DOWN,0); require(c.marks[0].from.x==61 && c.marks[0].from.y==61,"every object nudges one pixel per key"); if(!mark.points.empty()) require(c.marks[0].points[1].x==91 && c.marks[0].points[1].y==81,"nudge moves entire stroke"); SendMessageW(window,WM_KEYDOWN,VK_LEFT,0); SendMessageW(window,WM_KEYDOWN,VK_UP,0); require(c.marks[0].from.x==60 && c.marks[0].from.y==60,"opposite keys restore original position"); } ks::Mark nudgeArrow{{60,60},{140,140},0}; nudgeArrow.label=L"Caption"; nudgeArrow.fontSize=12; c.marks={nudgeArrow}; c.selected=0; c.selectedLabel=true; auto beforeNudge=ks::screenshotTextBounds(ks::screenshotArrowLabel(c.marks[0])); for(int i=0;i<5;++i) SendMessageW(window,WM_KEYDOWN,VK_RIGHT,0); auto afterNudge=ks::screenshotTextBounds(ks::screenshotArrowLabel(c.marks[0])); require(afterNudge.left==beforeNudge.left+5 && afterNudge.top==beforeNudge.top, "caption nudges exact screen pixels at a diagonal angle"); require(c.marks[0].from.x==60 && !c.marks[0].labelRotationManual,"caption nudge leaves arrow and automatic angle intact"); SendMessageW(window,WM_COMMAND,12,0); require(ks::screenshotTextBounds(ks::screenshotArrowLabel(c.marks[0])).left==beforeNudge.left+4,"undo reverses one nudge"); SendMessageW(window,WM_COMMAND,18,0); require(ks::screenshotTextBounds(ks::screenshotArrowLabel(c.marks[0])).left==beforeNudge.left+5,"redo restores one nudge"); SetWindowPos(window,nullptr,80,60,720,500,SWP_NOZORDER|SWP_SHOWWINDOW); ks::copy(window,c,[](HWND,const ks::Clip &clip)->DWORD { if(clip.formats.empty()) throw std::runtime_error("copy produced no image"); return 0; }); require(IsWindow(window) && c.copied,"copy publishes screenshot and keeps editor open"); c.selected=0; c.selectedLabel=false; SendMessageW(window,WM_KEYDOWN,VK_RIGHT,0); require(!c.copied,"editing after copy requires confirmation again"); c.copied=true; SendMessageW(window,WM_COMMAND,12,0); require(!c.copied,"undo after copy requires confirmation again"); c.copied=true; SendMessageW(window,WM_COMMAND,18,0); require(!c.copied,"redo after copy requires confirmation again"); SetPropW(window,L"ConfirmationCentered",nullptr); SetTimer(window,987,10,[](HWND owner,UINT,UINT_PTR id,DWORD) { EnumThreadWindows(GetCurrentThreadId(),[](HWND popup,LPARAM parent)->BOOL { wchar_t className[64]{}; GetClassNameW(popup,className,64); if(GetWindow(popup,GW_OWNER)==(HWND)parent && std::wstring(className)==L"#32770") { RECT ownerRect{},popupRect{}; GetWindowRect((HWND)parent,&ownerRect); GetWindowRect(popup,&popupRect); if(std::abs(ownerRect.left+ownerRect.right-popupRect.left-popupRect.right)<=4 && std::abs(ownerRect.top+ownerRect.bottom-popupRect.top-popupRect.bottom)<=4) SetPropW((HWND)parent,L"ConfirmationCentered",(HANDLE)1); PostMessageW(popup,TDM_CLICK_BUTTON,IDNO,0); } return TRUE; },(LPARAM)owner); KillTimer(owner,id); }); SendMessageW(window,WM_CLOSE,0,0); require(IsWindow(window),"declining close confirmation keeps the screenshot open"); require(GetPropW(window,L"ConfirmationCentered")!=nullptr,"confirmation is centered on editor instead of screen"); RemovePropW(window,L"ConfirmationCentered"); c.copied=true; SendMessageW(window,WM_KEYDOWN,VK_ESCAPE,0); require(IsWindow(window),"Escape does not close the screenshot editor"); SendMessageW(window,WM_CLOSE,0,0); require(!IsWindow(window),"copied screenshot closes without confirmation"); static bool published=false; static bool notificationAccepted=false; static HWND notificationWindow{}; static bool expectHidden=false; app.notify=[](const ks::Clip &) { if(!published) throw std::runtime_error("notification preceded clipboard publication"); if(expectHidden && IsWindowVisible(notificationWindow)) throw std::runtime_error("selection overlay is still visible when notification is sent"); return notificationAccepted; }; for(bool autoOpen : {true,false}) for (bool accepted : {true, false}) { app.autoOpenEditor=autoOpen; published=false; notificationAccepted=accepted; ks::Capture selection(app); selection.image=std::make_unique(100,100); PatBlt(selection.image->dc,0,0,100,100,WHITENESS); selection.start={10,20}; selection.end={70,60}; auto overlay=CreateWindowW(cls.lpszClassName,L"Selection test",WS_POPUP,0,0,100,100,app.window,nullptr,app.instance,&selection); notificationWindow=overlay; expectHidden=!autoOpen; ShowWindow(overlay,SW_SHOWNOACTIVATE); ks::finishSelection(overlay,selection,[](HWND,const ks::Clip &clip)->DWORD { auto pixels=ks::decodeImage(clip); if(pixels.width!=60 || pixels.height!=40) throw std::runtime_error("automatic copy did not crop selection"); published=true; return 0; }); if(!autoOpen && accepted) { require(published && !IsWindow(overlay),"notification-first closes selection after successful submission"); continue; } require(published && selection.copied && IsWindowVisible(overlay) && selection.editing, "selection opens editor whether notification is accepted or rejected"); require(selection.image->width==60 && selection.image->height==40, "automatically opened editor contains the selected region"); SendMessageW(overlay,WM_CLOSE,0,0); require(!IsWindow(overlay),"automatically copied screenshot closes without confirmation"); } DestroyWindow(app.window); app.window=nullptr; CoUninitialize(); std::cout<<"Passed screenshot editor UI integration checks\n"; } catch(const std::exception &e) { std::cerr<