// 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() { CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED); try { 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"); 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); } wchar_t windowTitle[128]{}; GetWindowTextW(window,windowTitle,128); require(std::wstring(windowTitle)==L"KSwitcher — Screenshot #1","title includes editor index without usage hints"); RECT client{}; GetClientRect(window,&client); require(c.view.bottom<=client.bottom-c.statusHeight,"image does not overlap status bar"); ks::updateStatus(c); require(c.statusText.find(L"Freehand")!=std::wstring::npos,"default tool hint is shown"); SendMessageW(GetDlgItem(window,13),WM_MOUSEMOVE,0,0); require(c.statusText.find(L"Ctrl+C")!=std::wstring::npos,"button hover shows action shortcut"); SendMessageW(GetDlgItem(window,13),WM_MOUSELEAVE,0,0); require(c.statusText.find(L"Freehand")!=std::wstring::npos,"button leave restores contextual hint"); 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"); ks::Mark text{{20,20},{20,20},0}; text.fontSize=18; ks::beginInlineText(window,c,text,-1); require(c.inlineText!=nullptr,"inline text control opens"); ks::updateStatus(c); require(c.statusText.find(L"Ctrl+Enter")!=std::wstring::npos,"inline editing hint shows commit and cancel keys"); SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"First line\r\nДругий рядок"); require(c.textDraft.text==L"First line\r\nДругий рядок","inline text changes update draft"); SendMessageW(window,ks::FinishTextMessage,1,(LPARAM)c.inlineText); require(!c.inlineText && c.marks.size()==1 && c.marks[0].text==text.text+L"First line\r\nДругий рядок","commit adds multiline text"); ks::beginInlineText(window,c,c.marks[0],0); SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Cancelled"); SendMessageW(window,ks::FinishTextMessage,2,(LPARAM)c.inlineText); require(c.marks[0].text!=L"Cancelled","Escape cancels changes"); SendMessageW(window,WM_KEYDOWN,VK_F2,0); require(c.inlineText!=nullptr,"F2 edits inline"); SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Edited"); ks::finishInlineText(window,c,true); require(c.marks[0].text==L"Edited","existing text edit commits"); SendMessageW(window,WM_COMMAND,12,0); require(c.marks[0].text!=L"Edited","undo restores original text"); SendMessageW(window,WM_COMMAND,18,0); require(c.marks[0].text==L"Edited","redo restores text edit"); ks::beginInlineText(window,c,c.marks[0],0); SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L""); ks::finishInlineText(window,c,true); require(c.marks.empty(),"committing empty text deletes object"); c.marks.push_back({{30,150},{260,150},0}); c.selected=0; SendMessageW(window,WM_COMMAND,35,0); require(c.inlineText && c.editingLabel,"label button opens inline editor"); SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Arrow caption"); ks::finishInlineText(window,c,true); require(c.marks.size()==1 && c.marks[0].label==L"Arrow caption" && c.marks[0].isLine(),"label attaches to arrow"); SendMessageW(window,WM_KEYDOWN,VK_F2,0); require(c.inlineText && c.editingLabel,"F2 edits arrow caption"); SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L""); ks::finishInlineText(window,c,true); require(c.marks.size()==1 && c.marks[0].label.empty(),"empty label removes caption without deleting arrow"); SendMessageW(window,WM_COMMAND,12,0); require(c.marks[0].label==L"Arrow caption","undo restores arrow caption"); auto caption=ks::screenshotArrowLabel(c.marks[0]); auto captionGrip=ks::screenshotTextRotationHandle(caption,double(c.unit*0.6)*c.image->width/(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; 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; app.notify=[](const ks::Clip &) { return published; }; 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); 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; }); require(published && selection.copied && !IsWindow(overlay),"selection copies before notification and closes without editor"); DestroyWindow(app.window); app.window=nullptr; CoUninitialize(); std::cout<<"Passed screenshot editor UI integration checks\n"; } catch(const std::exception &e) { std::cerr<