#include "screenshot.h" #include "ocr.h" #include "../resources/resource.h" #include #include #include #include #include namespace ks { namespace { // Only selection is exclusive; each completed capture owns an independent editor. HWND activeCapture{}; // Keep titles stable and distinct for every editor opened during this session. unsigned long long nextEditorIndex=1; bool confirmScreenshotClose(HWND owner) { TASKDIALOGCONFIG dialog{sizeof(dialog)}; dialog.hwndParent=owner; dialog.dwFlags=TDF_POSITION_RELATIVE_TO_WINDOW|TDF_ALLOW_DIALOG_CANCELLATION|TDF_USE_HICON_MAIN; const int iconSize=MulDiv(32,GetDpiForWindow(owner),96); auto icon=(HICON)LoadImageW(GetModuleHandleW(nullptr),MAKEINTRESOURCEW(IDI_SCREENSHOT_QUESTION), IMAGE_ICON,iconSize,iconSize,0); dialog.hMainIcon=icon; dialog.dwCommonButtons=TDCBF_YES_BUTTON|TDCBF_NO_BUTTON; dialog.pszWindowTitle=L"KSwitcher"; dialog.pszMainInstruction=L"Close without copying?"; dialog.pszContent=L"Your latest screenshot changes have not been copied to the clipboard."; dialog.nDefaultButton=IDNO; dialog.lpCallbackData=(LONG_PTR)owner; dialog.pfCallback=[](HWND window,UINT notification,WPARAM,LPARAM,LONG_PTR data)->HRESULT { if(notification==TDN_CREATED) { RECT parent{},popup{}; GetWindowRect((HWND)data,&parent); GetWindowRect(window,&popup); LONG width=popup.right-popup.left,height=popup.bottom-popup.top; LONG x=parent.left+(parent.right-parent.left-width)/2; LONG y=parent.top+(parent.bottom-parent.top-height)/2; MONITORINFO monitor{sizeof(monitor)}; if(GetMonitorInfoW(MonitorFromWindow((HWND)data,MONITOR_DEFAULTTONEAREST),&monitor)) { x=std::clamp(x,monitor.rcWork.left,std::max(monitor.rcWork.left,monitor.rcWork.right-width)); y=std::clamp(y,monitor.rcWork.top,std::max(monitor.rcWork.top,monitor.rcWork.bottom-height)); } SetWindowPos(window,nullptr,x,y,0,0,SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE); } return S_OK; }; int button=IDNO; auto result=TaskDialogIndirect(&dialog,&button,nullptr,nullptr); if(icon) DestroyIcon(icon); // If Windows cannot display the confirmation, keep the screenshot open. return SUCCEEDED(result) && button==IDYES; } constexpr int ToolIds[]{10,11,22,23,24,25,21,19,26,20,12,18,13}; struct Surface { HDC dc = CreateCompatibleDC(nullptr); HBITMAP bitmap{}; HGDIOBJ old{}; void *pixels{}; int width{}, height{}; Surface(int w, int h) : width(w), height(h) { if (w <= 0 || h <= 0 || uint64_t(w) * h > 128 * 1024 * 1024) { DeleteDC(dc); throw std::runtime_error("Screen image is too large"); } BITMAPINFO info{}; info.bmiHeader = {sizeof(BITMAPINFOHEADER), w, -h, 1, 32, BI_RGB}; bitmap = CreateDIBSection(dc, &info, DIB_RGB_COLORS, &pixels, nullptr, 0); if (!dc || !bitmap) { if (bitmap) DeleteObject(bitmap); if (dc) DeleteDC(dc); throw std::runtime_error("Cannot allocate screen image"); } old = SelectObject(dc, bitmap); } ~Surface() { SelectObject(dc, old); DeleteObject(bitmap); DeleteDC(dc); } Surface(const Surface &) = delete; Surface &operator=(const Surface &) = delete; }; using Mark = ScreenshotMark; HFONT textFont(const Mark &m) { return CreateFontW(-MulDiv(std::clamp(m.fontSize,6,200),96,72),0,0,0,m.fontWeight,m.fontItalic,FALSE,FALSE, DEFAULT_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH,m.fontName.c_str()); } RECT plainTextBounds(const Mark &m) { auto dc=CreateCompatibleDC(nullptr); auto font=textFont(m); auto previous=SelectObject(dc,font); RECT r{m.from.x,m.from.y,m.from.x,m.from.y}; DrawTextW(dc,m.text.c_str(),int(m.text.size()),&r,DT_CALCRECT|DT_LEFT|DT_TOP|DT_NOPREFIX|DT_EXPANDTABS); // Leave room for italic overhang without clipping the exported text. r.right+=std::max(2,m.fontSize/4); SelectObject(dc,previous); DeleteObject(font); DeleteDC(dc); return r; } POINT rotateTextPoint(const Mark &m,POINT p,double direction=1) { auto r=plainTextBounds(m); double cx=(r.left+r.right)/2.0,cy=(r.top+r.bottom)/2.0; double angle=m.rotation*direction*3.141592653589793/180; return {LONG(std::lround(cx+(p.x-cx)*std::cos(angle)-(p.y-cy)*std::sin(angle))), LONG(std::lround(cy+(p.x-cx)*std::sin(angle)+(p.y-cy)*std::cos(angle)))}; } bool chooseTextFont(HWND owner,Mark &m) { LOGFONTW font{}; font.lfHeight=-MulDiv(m.fontSize,GetDpiForWindow(owner),72); font.lfWeight=m.fontWeight; font.lfItalic=m.fontItalic; lstrcpynW(font.lfFaceName,m.fontName.c_str(),LF_FACESIZE); CHOOSEFONTW choose{sizeof(choose)}; choose.hwndOwner=owner; choose.lpLogFont=&font; choose.Flags=CF_SCREENFONTS|CF_INITTOLOGFONTSTRUCT|CF_LIMITSIZE; choose.nSizeMin=6; choose.nSizeMax=200; if(!ChooseFontW(&choose)) return false; m.fontName=font.lfFaceName; m.fontSize=std::clamp(choose.iPointSize/10,6,200); m.fontWeight=font.lfWeight; m.fontItalic=font.lfItalic!=0; return true; } struct Capture { ScreenshotHost &app; std::unique_ptr image; std::unique_ptr dimmed, frame; bool editing{}, dragging{}, arrow{}, owned{}, copied{}; bool autoOpenEditor; bool highlight{}; bool freehand=true; bool straight{}, textTool{}; Mark textDefaults{}; ScreenshotShape shape=ScreenshotShape::None; LineStyle lineStyle=LineStyle::Solid; LineEnds arrowEnds=LineEnds::End, freehandEnds=LineEnds::None; COLORREF currentColor=RGB(225,42,50); COLORREF customColors[16]{}; Mark stroke{}; POINT start{}, end{}; RECT selection{}, view{}; std::vector marks; ScreenshotHistory history; HWND editor{}, inlineText{}; HFONT inlineFont{}; Mark textDraft{}; int textIndex=-1; bool editingLabel=false; int selected = -1; bool selectedLabel=false; int wheelDelta = 0, wheelTarget = -1; MarkPart part = MarkPart::None; Mark original{}; std::vector beforeDrag; HFONT font{}; HICON toolIcons[3][15]{}; int iconSize{}; HCURSOR pencilCursor{}; int pencilCursorSize{}; int unit = 1, panelHeight = 1, statusHeight = 0; int hoveredTool=0; MarkPart hoveredPart=MarkPart::None; std::wstring statusText; explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {} ~Capture() { if(pencilCursor) DestroyCursor(pencilCursor); if (font) DeleteObject(font); if(inlineFont) DeleteObject(inlineFont); for(auto &variant:toolIcons) for(auto icon:variant) if(icon) DestroyIcon(icon); } void remember(const std::vector &previous) { copied=false; history.remember(previous); syncHistory(); } void syncHistory() { EnableWindow(GetDlgItem(editor,12),history.canUndo()); EnableWindow(GetDlgItem(editor,18),history.canRedo()); } }; void updateStatus(Capture &c) { auto status=GetDlgItem(c.editor,50); if(!status) return; std::wstring text; auto part=c.dragging?c.part:c.hoveredPart; if(c.hoveredTool && !c.dragging) { wchar_t title[256]{}; GetWindowTextW(GetDlgItem(c.editor,c.hoveredTool),title,256); text=title; if(c.hoveredTool==30) text=L"Line style: solid, dotted, dashed or dash-dot"; if(c.hoveredTool==31) text=L"Line ends: start, end, both or no arrowheads"; if(c.hoveredTool==35) text=L"Arrow caption: F2 / double-click to edit; drag to reposition"; } else if(c.inlineText) text=L"Text: Enter = new line | Ctrl+Enter = apply | Esc = cancel"; else if(part==MarkPart::Rotation) text=L"Drag to rotate text | Hold Shift for 15-degree steps"; else if(part==MarkPart::Thickness) text=L"Drag to change thickness | Mouse wheel over object = size"; else if(part==MarkPart::Radius) text=L"Drag to resize the number | Mouse wheel = diameter"; else if(part==MarkPart::Start || part==MarkPart::End) text=L"Drag handle to resize | Arrow endpoints can snap to numbered markers"; else if(c.selected>=0) { const auto &m=c.marks[c.selected]; if(c.selectedLabel || part==MarkPart::Label) text=L"Caption: arrows = 1 px | F2 = edit | Drag round handle = rotate"; 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 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.freehand) text=L"Freehand (F): drag to draw | Ctrl+drag = draw over an object"; else if(c.arrow) text=L"Drag to draw | Ctrl+drag = draw over an object | F2 on a line = caption"; else text=L"Number (N): click to add | Drag objects to move | Ctrl+C = copy screenshot"; if(text!=c.statusText) { c.statusText=std::move(text); SendMessageW(status,SB_SETTEXTW,0,(LPARAM)c.statusText.c_str()); SendMessageW(status,SB_SETTIPTEXTW,0,(LPARAM)c.statusText.c_str()); } } RECT region(POINT a, POINT b) { return {std::min(a.x,b.x), std::min(a.y,b.y), std::max(a.x,b.x), std::max(a.y,b.y)}; } constexpr UINT FinishTextMessage=WM_APP+73; void readInlineText(Capture &c) { if(!c.inlineText) return; int length=GetWindowTextLengthW(c.inlineText); std::wstring text(size_t(length)+1,L'\0'); GetWindowTextW(c.inlineText,text.data(),length+1); text.resize(length); c.textDraft.text=std::move(text); } void positionInlineText(Capture &c) { if(!c.inlineText) return; auto sample=c.textDraft; if(sample.text.empty()) sample.text=L" "; auto bounds=plainTextBounds(sample); int viewWidth=std::max(1L,c.view.right-c.view.left); int x=c.view.left+MulDiv(sample.from.x,viewWidth,c.image->width); int y=c.view.top+MulDiv(sample.from.y,viewWidth,c.image->width); auto old=c.inlineFont; auto sourceFont=textFont(sample); LOGFONTW font{}; GetObjectW(sourceFont,sizeof(font),&font); DeleteObject(sourceFont); font.lfHeight=-std::max(1,MulDiv(-font.lfHeight,viewWidth,c.image->width)); c.inlineFont=CreateFontIndirectW(&font); SendMessageW(c.inlineText,WM_SETFONT,(WPARAM)c.inlineFont,TRUE); if(old) DeleteObject(old); RECT client{}; GetClientRect(c.editor,&client); int width=std::min(std::max(120,MulDiv(bounds.right-bounds.left,viewWidth,c.image->width)+24),std::max(32,int(client.right)-x)); 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); } void finishInlineText(HWND w,Capture &c,bool commit) { if(!c.inlineText) return; readInlineText(c); auto input=c.inlineText; c.inlineText=nullptr; DestroyWindow(input); if(commit) { bool empty=c.textDraft.text.find_first_not_of(L" \r\n\t")==std::wstring::npos; if(c.editingLabel && c.textIndex>=0) { auto &arrow=c.marks[c.textIndex]; auto label=empty?std::wstring{}:c.textDraft.text; if(arrow.label!=label) { c.remember(c.marks); arrow.label=std::move(label); } c.selected=c.textIndex; } else if(c.textIndex>=0) { if(empty || c.marks[c.textIndex].text!=c.textDraft.text) { c.remember(c.marks); if(empty) { c.marks.erase(c.marks.begin()+c.textIndex); c.selected=-1; } else { c.marks[c.textIndex]=c.textDraft; c.selected=c.textIndex; } } } else if(!empty) { 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; } c.textIndex=-1; c.editingLabel=false; InvalidateRect(w,nullptr,FALSE); } LRESULT CALLBACK inlineTextProc(HWND w,UINT msg,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR) { if(msg==WM_KEYDOWN && (wp==VK_ESCAPE || (wp==VK_RETURN && (GetKeyState(VK_CONTROL)&0x8000)))) { PostMessageW(GetParent(w),FinishTextMessage,wp==VK_ESCAPE?2:1,(LPARAM)w); return 0; } if(msg==WM_CHAR && (wp==VK_ESCAPE || (wp==VK_RETURN && (GetKeyState(VK_CONTROL)&0x8000)))) return 0; return DefSubclassProc(w,msg,wp,lp); } void beginInlineText(HWND w,Capture &c,const Mark &mark,int index) { finishInlineText(w,c,true); c.textDraft=mark; c.textIndex=index; c.editingLabel=false; c.inlineText=CreateWindowExW(WS_EX_CLIENTEDGE,L"EDIT",mark.text.c_str(), WS_CHILD|WS_VISIBLE|WS_TABSTOP|ES_MULTILINE|ES_AUTOVSCROLL|ES_AUTOHSCROLL|ES_WANTRETURN, 0,0,1,1,w,(HMENU)40,c.app.instance,nullptr); if(!c.inlineText) throw std::runtime_error("Cannot open inline text editor"); SetWindowSubclass(c.inlineText,inlineTextProc,1,0); SendMessageW(c.inlineText,EM_SETLIMITTEXT,16384,0); positionInlineText(c); SetFocus(c.inlineText); SendMessageW(c.inlineText,EM_SETSEL,0,-1); InvalidateRect(w,nullptr,FALSE); } void beginMarkText(HWND w,Capture &c,int index) { if(c.marks[index].isText()) { beginInlineText(w,c,c.marks[index],index); return; } if(c.marks[index].isLine()) { c.selectedLabel=true; auto label=screenshotArrowLabel(c.marks[index]); beginInlineText(w,c,label,index); c.editingLabel=true; } } RECT markBounds(const Mark &m) { if(m.isText()) return screenshotTextBounds(m); RECT r=region(m.from,m.to); for(auto p:m.points) { r.left=std::min(r.left,p.x); r.top=std::min(r.top,p.y); r.right=std::max(r.right,p.x); r.bottom=std::max(r.bottom,p.y); } return r; } std::pair thicknessSegment(const Mark &m) { if(m.shape!=ScreenshotShape::None) { auto r=region(m.from,m.to); return {{r.right,r.top},{r.left,r.top}}; } if(m.freehand && m.points.size()>1) { size_t index=(m.points.size()-1)/2; return {m.points[index],m.points[index+1]}; } return {m.from,m.to}; } void drawHighlight(HDC dc,const Mark &m) { DIBSECTION target{}; if(!GetObjectW(GetCurrentObject(dc,OBJ_BITMAP),sizeof(target),&target) || !target.dsBm.bmBits) return; auto r=markBounds(m); int width=std::clamp(m.thickness,4,100),radius=(width+1)/2; r.left=std::max(0L,r.left-radius-1); r.top=std::max(0L,r.top-radius-1); r.right=std::min(target.dsBm.bmWidth,r.right+radius+2); r.bottom=std::min(target.dsBm.bmHeight,r.bottom+radius+2); if(r.right<=r.left || r.bottom<=r.top) return; Surface mask(r.right-r.left,r.bottom-r.top); PatBlt(mask.dc,0,0,mask.width,mask.height,BLACKNESS); LOGBRUSH white{BS_SOLID,RGB(255,255,255),0}; auto pen=ExtCreatePen(PS_GEOMETRIC|PS_SOLID|PS_ENDCAP_ROUND|PS_JOIN_ROUND,width,&white,0,nullptr); auto old=SelectObject(mask.dc,pen); auto brush=SelectObject(mask.dc,GetStockObject(WHITE_BRUSH)); POINT last{m.points[0].x-r.left,m.points[0].y-r.top}; MoveToEx(mask.dc,last.x,last.y,nullptr); for(auto p:m.points) LineTo(mask.dc,p.x-r.left,p.y-r.top); // A click without dragging is a round dot too. SelectObject(mask.dc,GetStockObject(NULL_PEN)); Ellipse(mask.dc,last.x-radius,last.y-radius,last.x+radius,last.y+radius); SelectObject(mask.dc,old); SelectObject(mask.dc,brush); DeleteObject(pen); GdiFlush(); auto pixels=static_cast(target.dsBm.bmBits); auto coverage=static_cast(mask.pixels); unsigned channels[]{GetBValue(m.color),GetGValue(m.color),GetRValue(m.color)}; for(int y=0;y(at[channel]*(153+channels[channel]*102/255)/255); } } void drawMarks(HDC dc, const std::vector &marks, const Mark *preview = nullptr) { for(const auto &m:marks) if(m.isHighlight()) drawHighlight(dc,m); if(preview && preview->isHighlight()) drawHighlight(dc,*preview); auto pen = CreatePen(PS_SOLID, 3, RGB(225, 42, 50)); auto brush = CreateSolidBrush(RGB(225, 42, 50)); auto font = CreateFontW(-20, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH, L"Segoe UI"); auto oldPen = SelectObject(dc, pen), oldBrush = SelectObject(dc, brush), oldFont = SelectObject(dc, font); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, RGB(255,255,255)); auto draw = [&](const Mark &m) { auto markPen=CreatePen(PS_SOLID,3,m.color); auto markBrush=CreateSolidBrush(m.color); auto previousMarkPen=SelectObject(dc,markPen),previousMarkBrush=SelectObject(dc,markBrush); int brightness=299*GetRValue(m.color)+587*GetGValue(m.color)+114*GetBValue(m.color); SetTextColor(dc,brightness>=150000?RGB(20,20,20):RGB(255,255,255)); if(m.isText()) { auto font=textFont(m); auto previous=SelectObject(dc,font); SetTextColor(dc,m.color); auto bounds=plainTextBounds(m); int saved=SaveDC(dc); SetGraphicsMode(dc,GM_ADVANCED); double angle=m.rotation*3.141592653589793/180; double cs=std::cos(angle),sn=std::sin(angle),cx=(bounds.left+bounds.right)/2.0,cy=(bounds.top+bounds.bottom)/2.0; XFORM transform{FLOAT(cs),FLOAT(sn),FLOAT(-sn),FLOAT(cs),FLOAT(cx-cs*cx+sn*cy),FLOAT(cy-sn*cx-cs*cy)}; SetWorldTransform(dc,&transform); DrawTextW(dc,m.text.c_str(),int(m.text.size()),&bounds,DT_LEFT|DT_TOP|DT_NOPREFIX|DT_EXPANDTABS); RestoreDC(dc,saved); SelectObject(dc,previous); DeleteObject(font); } else if (m.number) { int radius = std::clamp(m.radius,8,200); auto numberFont = CreateFontW(-std::max(10,radius*4/3),0,0,0,FW_BOLD,FALSE,FALSE,FALSE, DEFAULT_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH,L"Segoe UI"); auto previousFont=SelectObject(dc,numberFont); Ellipse(dc, m.from.x-radius, m.from.y-radius, m.from.x+radius+1, m.from.y+radius+1); RECT r{m.from.x-radius, m.from.y-radius, m.from.x+radius+1, m.from.y+radius+1}; auto text = std::to_wstring(m.number); DrawTextW(dc, text.c_str(), -1, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE); SelectObject(dc,previousFont); DeleteObject(numberFont); } else { int thickness=std::clamp(m.thickness,1,30); DWORD pattern[4]{}; DWORD count=0; if(m.style==LineStyle::Dotted) { pattern[0]=1; pattern[1]=3; count=2; } if(m.style==LineStyle::Dashed) { pattern[0]=5; pattern[1]=3; count=2; } if(m.style==LineStyle::DashDot) { pattern[0]=5; pattern[1]=3; pattern[2]=1; pattern[3]=3; count=4; } for(DWORD i=0;i1) Polyline(dc,m.points.data(),int(m.points.size())); else { MoveToEx(dc,m.from.x,m.from.y,nullptr); LineTo(dc,m.to.x,m.to.y); } if(m.freehand && m.points.size()==1) { SelectObject(dc,GetStockObject(NULL_PEN)); int radius=std::max(1,thickness/2); Ellipse(dc,m.from.x-radius,m.from.y-radius,m.from.x+radius+1,m.from.y+radius+1); } SelectObject(dc,previousPen); DeleteObject(arrowPen); auto headPen=CreatePen(PS_SOLID,thickness,m.color); previousPen=SelectObject(dc,headPen); double headSize=std::max(10,thickness*4+4); auto head=[&](POINT tip,POINT previous) { if(tip.x==previous.x && tip.y==previous.y) return; double angle=std::atan2(double(tip.y-previous.y),double(tip.x-previous.x)); POINT triangle[]{tip, {tip.x-LONG(headSize*std::cos(angle-0.45)),tip.y-LONG(headSize*std::sin(angle-0.45))}, {tip.x-LONG(headSize*std::cos(angle+0.45)),tip.y-LONG(headSize*std::sin(angle+0.45))}}; Polygon(dc,triangle,3); }; POINT afterStart=m.to,beforeEnd=m.from; if(m.freehand) { // A single mouse sample can point backwards due to initial jitter. // Use the path over the size of the head to determine its direction. auto direction=[&](auto begin,auto end,POINT tip) { POINT previous=tip, result=tip; double travelled=0; for(auto it=begin;it!=end;++it) { double length=std::hypot(double(it->x-previous.x),double(it->y-previous.y)); if(length>0 && travelled+length>=headSize) { double fraction=(headSize-travelled)/length; return POINT{previous.x+LONG(std::lround((it->x-previous.x)*fraction)), previous.y+LONG(std::lround((it->y-previous.y)*fraction))}; } travelled+=length; previous=*it; if(it->x!=tip.x || it->y!=tip.y) result=*it; } return result; }; afterStart=direction(m.points.begin(),m.points.end(),m.from); beforeEnd=direction(m.points.rbegin(),m.points.rend(),m.to); } if(m.ends==LineEnds::Start || m.ends==LineEnds::Both) head(m.from,afterStart); if(m.ends==LineEnds::End || m.ends==LineEnds::Both) head(m.to,beforeEnd); SelectObject(dc,previousPen); DeleteObject(headPen); } } SelectObject(dc,previousMarkPen); SelectObject(dc,previousMarkBrush); DeleteObject(markPen); DeleteObject(markBrush); }; for (const auto &m : marks) if (!m.number && !m.isHighlight()) draw(m); if (preview && !preview->isHighlight()) draw(*preview); for (const auto &m : marks) if (m.number) draw(m); for(const auto &m:marks) if(m.isLine() && !m.label.empty()) draw(screenshotArrowLabel(m)); SelectObject(dc, oldFont); SelectObject(dc, oldBrush); SelectObject(dc, oldPen); DeleteObject(font); DeleteObject(brush); DeleteObject(pen); } bool selectedLine(const Capture &c) { return c.selected>=0 && !c.marks[c.selected].number && !c.marks[c.selected].isHighlight() && !c.marks[c.selected].isText(); } void syncLineOptions(HWND w,const Capture &c) { EnableWindow(GetDlgItem(w,34),c.textTool || (c.selected>=0 && (c.marks[c.selected].isText() || !c.marks[c.selected].label.empty()))); EnableWindow(GetDlgItem(w,35),c.selected>=0 && c.marks[c.selected].isLine()); bool selected=selectedLine(c),enabled=selected || c.arrow || c.freehand; int style=int(selected?c.marks[c.selected].style:c.lineStyle); int ends=int(selected?c.marks[c.selected].ends:c.freehand?c.freehandEnds:c.straight || c.shape!=ScreenshotShape::None?LineEnds::None:c.arrowEnds); EnableWindow(GetDlgItem(w,30),enabled); EnableWindow(GetDlgItem(w,31),enabled && (selected?c.marks[c.selected].shape==ScreenshotShape::None:!c.straight && c.shape==ScreenshotShape::None)); if(SendDlgItemMessageW(w,30,CB_GETCURSEL,0,0)!=style) SendDlgItemMessageW(w,30,CB_SETCURSEL,style,0); if(SendDlgItemMessageW(w,31,CB_GETCURSEL,0,0)!=ends) SendDlgItemMessageW(w,31,CB_SETCURSEL,ends,0); } void layout(HWND w, Capture &c) { if (!c.editing) return; RECT r; GetClientRect(w, &r); int dpi = GetDpiForWindow(w); int cursorSize=GetSystemMetricsForDpi(SM_CXCURSOR,dpi); if(cursorSize!=c.pencilCursorSize) { auto cursor=(HCURSOR)LoadImageW(c.app.instance,MAKEINTRESOURCEW(IDC_SCREENSHOT_PENCIL),IMAGE_CURSOR,cursorSize,cursorSize,0); if(cursor) { if(c.pencilCursor) DestroyCursor(c.pencilCursor); c.pencilCursor=cursor; c.pencilCursorSize=cursorSize; } } int iconSize=MulDiv(18,dpi,96); if(iconSize!=c.iconSize) { c.iconSize=iconSize; for(int variant=0;variant<3;++variant) for(int i=0;i<15;++i) { if(c.toolIcons[variant][i]) DestroyIcon(c.toolIcons[variant][i]); c.toolIcons[variant][i]=(HICON)LoadImageW(c.app.instance,MAKEINTRESOURCEW(i<9?300+variant*10+i:400+variant*10+i-9),IMAGE_ICON,iconSize,iconSize,0); } } if (c.font) DeleteObject(c.font); c.font = CreateFontW(-MulDiv(14,dpi,96),0,0,0,FW_NORMAL,FALSE,FALSE,FALSE,DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,CLEARTYPE_QUALITY,DEFAULT_PITCH,L"Segoe UI"); c.unit = std::max(1, MulDiv(44, dpi, 96)); auto status=GetDlgItem(w,50); SendMessageW(status,WM_SETFONT,(WPARAM)c.font,TRUE); SendMessageW(status,WM_SIZE,0,0); 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,380,416,464}; static_assert(std::size(positions)==std::size(ToolIds)); for (size_t i = 0; i < std::size(ToolIds); ++i) { SendMessageW(GetDlgItem(w,ToolIds[i]),WM_SETFONT,(WPARAM)c.font,TRUE); MoveWindow(GetDlgItem(w,ToolIds[i]),MulDiv(positions[i],dpi,96),MulDiv(6,dpi,96), MulDiv(32,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); MoveWindow(GetDlgItem(w,32),MulDiv(12,dpi,96),c.unit+MulDiv(6,dpi,96),MulDiv(64,dpi,96),MulDiv(24,dpi,96),TRUE); MoveWindow(GetDlgItem(w,30),MulDiv(80,dpi,96),c.unit+MulDiv(2,dpi,96),MulDiv(150,dpi,96),MulDiv(180,dpi,96),TRUE); MoveWindow(GetDlgItem(w,33),MulDiv(248,dpi,96),c.unit+MulDiv(6,dpi,96),MulDiv(40,dpi,96),MulDiv(24,dpi,96),TRUE); MoveWindow(GetDlgItem(w,31),MulDiv(294,dpi,96),c.unit+MulDiv(2,dpi,96),MulDiv(170,dpi,96),MulDiv(180,dpi,96),TRUE); MoveWindow(GetDlgItem(w,34),MulDiv(480,dpi,96),c.unit+MulDiv(2,dpi,96),MulDiv(28,dpi,96),MulDiv(28,dpi,96),TRUE); MoveWindow(GetDlgItem(w,35),MulDiv(516,dpi,96),c.unit+MulDiv(2,dpi,96),MulDiv(28,dpi,96),MulDiv(28,dpi,96),TRUE); syncLineOptions(w,c); double ratio = std::min({1.0, double(std::max(1L,r.right-16))/c.image->width, double(std::max(1L,r.bottom-c.panelHeight-16))/c.image->height}); int width = int(c.image->width*ratio), height = int(c.image->height*ratio); c.view = {(r.right-width)/2, c.panelHeight+(r.bottom-c.panelHeight-height)/2, (r.right+width)/2, c.panelHeight+(r.bottom-c.panelHeight+height)/2}; InvalidateRect(w, nullptr, FALSE); } POINT imagePoint(Capture &c, POINT p, bool clamp = true) { POINT result{MulDiv(p.x-c.view.left,c.image->width,std::max(1L,c.view.right-c.view.left)), MulDiv(p.y-c.view.top,c.image->height,std::max(1L,c.view.bottom-c.view.top))}; if(clamp) { result.x=std::clamp(result.x,0L,LONG(c.image->width-1)); result.y=std::clamp(result.y,0L,LONG(c.image->height-1)); } return result; } POINT thicknessHandle(const Capture &c, const Mark &m) { return screenshotThicknessHandle(m,double(c.unit*0.4)*c.image->width/std::max(1L,c.view.right-c.view.left)); } std::pair markAt(Capture &c, POINT p) { if((c.arrow || c.highlight || c.freehand || c.textTool) && (GetKeyState(VK_CONTROL)&0x8000)) return {-1,MarkPart::None}; double tolerance=double(std::max(6,c.unit/6))*c.image->width/std::max(1L,c.view.right-c.view.left); if(c.selected>=0) { const auto &m=c.marks[c.selected]; if(m.isText() || (m.isLine() && !m.label.empty())) { auto text=m.isText()?m:screenshotArrowLabel(m); auto grip=screenshotTextRotationHandle(text,double(c.unit*0.6)*c.image->width/std::max(1L,c.view.right-c.view.left)); if(std::hypot(double(p.x-grip.x),double(p.y-grip.y))<=tolerance) return {c.selected,MarkPart::Rotation}; } auto handle=thicknessHandle(c,m); if(!m.number && !m.isHighlight() && !m.isText() && std::hypot(double(p.x-handle.x),double(p.y-handle.y))<=tolerance) return {c.selected,MarkPart::Thickness}; auto part=hitScreenshotMark(c.marks[c.selected],p,tolerance); if(part!=MarkPart::None) return {c.selected,part}; } for(int i=int(c.marks.size())-1;i>=0;--i) { auto part=hitScreenshotMark(c.marks[i],p,tolerance); if(part!=MarkPart::None) return {i,part}; } return {-1,MarkPart::None}; } void captureCursor(HWND w, Capture &c) { LPCWSTR cursor=IDC_CROSS; if(c.editing) { POINT p; GetCursorPos(&p); ScreenToClient(w,&p); auto hit=c.dragging?std::pair{c.selected,c.part}:markAt(c,imagePoint(c,p,false)); if(!c.dragging && !PtInRect(&c.view,p) && hit.second!=MarkPart::Thickness && hit.second!=MarkPart::Rotation) hit={-1,MarkPart::None}; if(!c.dragging && !PtInRect(&c.view,p)) cursor=IDC_ARROW; if(hit.second==MarkPart::Body || hit.second==MarkPart::Label) cursor=IDC_SIZEALL; else if(hit.second==MarkPart::Rotation) cursor=IDC_HAND; else if(hit.second==MarkPart::Radius) cursor=IDC_SIZEWE; else if(hit.second==MarkPart::Start || hit.second==MarkPart::End || hit.second==MarkPart::Thickness) { const auto &mark=c.marks[hit.first]; auto [from,to]=hit.second==MarkPart::Thickness?thicknessSegment(mark):std::pair{mark.from,mark.to}; double angle=std::atan2(double(to.y-from.y),double(to.x-from.x)); if(hit.second==MarkPart::Thickness) angle+=3.141592653589793/2; int axis=(int(std::lround(angle/(3.141592653589793/4)))+8)%4; const LPCWSTR cursors[]{IDC_SIZEWE,IDC_SIZENWSE,IDC_SIZENS,IDC_SIZENESW}; cursor=cursors[axis]; } } SetCursor(c.editing && c.freehand && cursor==IDC_CROSS && c.pencilCursor?c.pencilCursor:LoadCursorW(nullptr,cursor)); if(c.editing) { POINT p; GetCursorPos(&p); ScreenToClient(w,&p); c.hoveredPart=markAt(c,imagePoint(c,p,false)).second; if(!PtInRect(&c.view,p) && c.hoveredPart!=MarkPart::Rotation && c.hoveredPart!=MarkPart::Thickness) c.hoveredPart=MarkPart::None; updateStatus(c); } } void drawHandles(HDC dc, const Capture &c) { if (c.inlineText || c.selected < 0 || c.selected >= int(c.marks.size())) return; const auto &m=c.marks[c.selected]; auto point=[&](POINT p) -> POINT { return {c.view.left+MulDiv(p.x,c.view.right-c.view.left,c.image->width), c.view.top+MulDiv(p.y,c.view.bottom-c.view.top,c.image->height)}; }; auto pen=CreatePen(PS_SOLID,2,RGB(0,100,220)); auto oldPen=SelectObject(dc,pen), oldBrush=SelectObject(dc,GetStockObject(WHITE_BRUSH)); auto handle=[&](POINT p) { p=point(p); int size=std::max(4,c.unit/8); Rectangle(dc,p.x-size,p.y-size,p.x+size+1,p.y+size+1); }; auto textHandles=[&](const Mark &text) { auto r=plainTextBounds(text); POINT corners[]{point(rotateTextPoint(text,{r.left,r.top})),point(rotateTextPoint(text,{r.right,r.top})), point(rotateTextPoint(text,{r.right,r.bottom})),point(rotateTextPoint(text,{r.left,r.bottom})),point(rotateTextPoint(text,{r.left,r.top}))}; Polyline(dc,corners,5); auto top=point(rotateTextPoint(text,{(r.left+r.right)/2,r.top})); auto grip=screenshotTextRotationHandle(text,double(c.unit*0.6)*c.image->width/std::max(1L,c.view.right-c.view.left)); auto end=point(grip); MoveToEx(dc,top.x,top.y,nullptr); LineTo(dc,end.x,end.y); int radius=std::max(4,c.unit/8); Ellipse(dc,end.x-radius,end.y-radius,end.x+radius+1,end.y+radius+1); }; if(m.isText()) textHandles(m); else if(m.isHighlight()) { auto bounds=markBounds(m); auto a=point({bounds.left-m.thickness/2,bounds.top-m.thickness/2}); auto b=point({bounds.right+m.thickness/2,bounds.bottom+m.thickness/2}); RECT r{a.x-3,a.y-3,b.x+4,b.y+4}; DrawFocusRect(dc,&r); } else if (m.number) { auto a=point({m.from.x-m.radius,m.from.y-m.radius}); auto b=point({m.from.x+m.radius,m.from.y+m.radius}); auto white=SelectObject(dc,GetStockObject(HOLLOW_BRUSH)); Ellipse(dc,a.x-2,a.y-2,b.x+3,b.y+3); SelectObject(dc,white); handle({m.from.x+m.radius,m.from.y}); } else { handle(m.from); handle(m.to); auto [from,to]=thicknessSegment(m); auto mid=point({(from.x+to.x)/2,(from.y+to.y)/2}); auto grip=thicknessHandle(c,m),end=point(grip); MoveToEx(dc,mid.x,mid.y,nullptr); LineTo(dc,end.x,end.y); handle(grip); } if(m.isLine() && !m.label.empty()) textHandles(screenshotArrowLabel(m)); SelectObject(dc,oldBrush); SelectObject(dc,oldPen); DeleteObject(pen); } void drawTool(const DRAWITEMSTRUCT &item, const Capture &c) { auto dc=item.hDC; 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)|| (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); bool disabled=item.itemState&ODS_DISABLED; POINT mouse; GetCursorPos(&mouse); ScreenToClient(item.hwndItem,&mouse); bool hover=PtInRect(&r,mouse) && !disabled; bool pressed=item.itemState&ODS_SELECTED; FillRect(dc,&r,static_cast(GetStockObject(WHITE_BRUSH))); COLORREF fill=pressed?RGB(219,234,254):active?RGB(239,246,255):hover?RGB(241,245,249):RGB(255,255,255); auto brush=CreateSolidBrush(fill); auto pen=CreatePen(PS_SOLID,1,fill); auto oldPen=SelectObject(dc,pen),oldBrush=SelectObject(dc,brush); int corner=MulDiv(12,GetDpiForWindow(item.hwndItem),96); 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=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(item.CtlID==20) { auto color=c.selected>=0?c.marks[c.selected].color:c.currentColor; auto swatch=CreateSolidBrush(color); RECT stripe{r.right/4,r.bottom-5,r.right*3/4,r.bottom-2}; FillRect(dc,&stripe,swatch); DeleteObject(swatch); } if(item.itemState&ODS_FOCUS) { InflateRect(&r,-3,-3); DrawFocusRect(dc,&r); } } LRESULT CALLBACK toolHover(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR) { auto c=reinterpret_cast(GetWindowLongPtrW(GetParent(w),GWLP_USERDATA)); if(c && (msg==WM_MOUSEMOVE || msg==WM_SETFOCUS)) { c->hoveredTool=GetDlgCtrlID(w); updateStatus(*c); } if(c && (msg==WM_MOUSELEAVE || msg==WM_KILLFOCUS) && c->hoveredTool==GetDlgCtrlID(w)) { c->hoveredTool=0; updateStatus(*c); } if(msg==WM_SETCURSOR && LOWORD(lp)==HTCLIENT) { SetCursor(LoadCursorW(nullptr,IsWindowEnabled(w)?IDC_HAND:IDC_ARROW)); return TRUE; } if(msg==WM_MOUSEMOVE) { TRACKMOUSEEVENT track{sizeof(track),TME_LEAVE,w,0}; TrackMouseEvent(&track); InvalidateRect(w,nullptr,FALSE); } if(msg==WM_MOUSELEAVE) InvalidateRect(w,nullptr,FALSE); return DefSubclassProc(w,msg,wp,lp); } void copy(HWND w, Capture &c, DWORD (*publish)(HWND,const Clip &)=writeClipboard) { finishInlineText(w,c,true); // The resident message window keeps ownership if the editor is closed later. auto clip=screenshotClip(c.image->dc,c.image->width,c.image->height,c.marks); publish(c.app.window, clip); c.copied=true; } void edit(HWND w, Capture &c) { auto r = region(c.start,c.end); if (r.right-r.left < 2 || r.bottom-r.top < 2) return; auto cropped = std::make_unique(r.right-r.left,r.bottom-r.top); BitBlt(cropped->dc,0,0,cropped->width,cropped->height,c.image->dc,r.left,r.top,SRCCOPY); c.image = std::move(cropped); c.dimmed.reset(); c.frame.reset(); c.editing = true; if (activeCapture == w) activeCapture = nullptr; MONITORINFO monitor{sizeof(monitor)}; GetMonitorInfoW(MonitorFromWindow(w,MONITOR_DEFAULTTOPRIMARY), &monitor); POINT cursor; GetCursorPos(&cursor); GetMonitorInfoW(MonitorFromPoint(cursor,MONITOR_DEFAULTTOPRIMARY), &monitor); ShowWindow(w,SW_HIDE); SetWindowLongPtrW(w,GWLP_HWNDPARENT,0); SetWindowLongPtrW(w,GWL_EXSTYLE,(GetWindowLongPtrW(w,GWL_EXSTYLE)|WS_EX_APPWINDOW)&~WS_EX_TOOLWINDOW); SetWindowLongPtrW(w,GWL_STYLE,WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN); auto title=L"KSwitcher — Screenshot #"+std::to_wstring(nextEditorIndex++); SetWindowTextW(w,title.c_str()); INITCOMMONCONTROLSEX statusControls{sizeof(statusControls),ICC_BAR_CLASSES}; 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"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", 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 tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,w,nullptr,c.app.instance,nullptr); for (size_t i=0;i(labels[i]); SendMessageW(tips,TTM_ADDTOOLW,0,(LPARAM)&tool); } SendMessageW(GetDlgItem(w,21),BM_SETCHECK,BST_CHECKED,0); CreateWindowW(L"STATIC",L"Line style",WS_CHILD|WS_VISIBLE,0,0,0,0,w,(HMENU)32,c.app.instance,nullptr); CreateWindowW(L"STATIC",L"Ends",WS_CHILD|WS_VISIBLE,0,0,0,0,w,(HMENU)33,c.app.instance,nullptr); for(int id:{30,31}) CreateWindowW(L"COMBOBOX",L"",WS_CHILD|WS_VISIBLE|CBS_DROPDOWNLIST|WS_VSCROLL, 0,0,0,0,w,(HMENU)(INT_PTR)id,c.app.instance,nullptr); for(auto text:{L"Solid",L"Dotted",L"Dashed",L"Dash-dot"}) SendDlgItemMessageW(w,30,CB_ADDSTRING,0,(LPARAM)text); for(auto text:{L"Arrow at end",L"Arrow at start",L"Arrows at both ends",L"No arrows"}) SendDlgItemMessageW(w,31,CB_ADDSTRING,0,(LPARAM)text); auto textButton=CreateWindowW(L"BUTTON",L"Font and size",WS_CHILD|WS_VISIBLE|BS_OWNERDRAW,0,0,0,0,w,(HMENU)34,c.app.instance,nullptr); SetWindowSubclass(textButton,toolHover,1,0); TOOLINFOW fontTip{sizeof(fontTip)}; fontTip.uFlags=TTF_IDISHWND|TTF_SUBCLASS; fontTip.hwnd=w; fontTip.uId=(UINT_PTR)textButton; fontTip.lpszText=const_cast(L"Font and size"); SendMessageW(tips,TTM_ADDTOOLW,0,(LPARAM)&fontTip); auto labelButton=CreateWindowW(L"BUTTON",L"Arrow label",WS_CHILD|WS_VISIBLE|BS_OWNERDRAW,0,0,0,0,w,(HMENU)35,c.app.instance,nullptr); SetWindowSubclass(labelButton,toolHover,1,0); fontTip.uId=(UINT_PTR)labelButton; fontTip.lpszText=const_cast(L"Arrow label (F2 / double-click); drag label to reposition"); SendMessageW(tips,TTM_ADDTOOLW,0,(LPARAM)&fontTip); c.syncHistory(); auto work = monitor.rcWork; SetWindowPos(w,HWND_NOTOPMOST,work.left+20,work.top+20, std::min(LONG(MulDiv(1000,GetDpiForWindow(w),96)),work.right-work.left-40), std::min(LONG(MulDiv(700,GetDpiForWindow(w),96)),work.bottom-work.top-40),SWP_FRAMECHANGED|SWP_SHOWWINDOW); layout(w,c); SetForegroundWindow(w); SetFocus(w); } void finishSelection(HWND w,Capture &c,DWORD (*publish)(HWND,const Clip &)=writeClipboard) { if(!c.app.notify) { edit(w,c); return; } auto r=region(c.start,c.end); if(r.right-r.left<2 || r.bottom-r.top<2) return; Surface cropped(r.right-r.left,r.bottom-r.top); BitBlt(cropped.dc,0,0,cropped.width,cropped.height,c.image->dc,r.left,r.top,SRCCOPY); auto clip=screenshotClip(cropped.dc,cropped.width,cropped.height,{}); publish(c.app.window,clip); c.copied=true; // A successful toast submission does not guarantee that Windows shows a banner. if(c.autoOpenEditor) edit(w,c); bool notified=c.app.notify(clip); if(!c.autoOpenEditor) { if(notified) DestroyWindow(w); else edit(w,c); } } LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { auto c = reinterpret_cast(GetWindowLongPtrW(w,GWLP_USERDATA)); if (msg == WM_NCCREATE) { c = static_cast(reinterpret_cast(lp)->lpCreateParams); c->editor=w; SetWindowLongPtrW(w,GWLP_USERDATA,(LONG_PTR)c); } if (!c) return DefWindowProcW(w,msg,wp,lp); try { switch (msg) { case WM_CLOSE: finishInlineText(w,*c,true); if(c->editing && (!c->copied || c->dragging) && !confirmScreenshotClose(w)) return 0; DestroyWindow(w); return 0; case FinishTextMessage: if(c->inlineText && (HWND)lp==c->inlineText && (wp!=0 || GetFocus()!=c->inlineText)) { finishInlineText(w,*c,wp!=2); if(wp!=0) SetFocus(w); } return 0; case WM_CTLCOLOREDIT: if((HWND)lp==c->inlineText) { SetTextColor((HDC)wp,c->textDraft.color); SetBkColor((HDC)wp,RGB(255,255,255)); return (LRESULT)GetStockObject(WHITE_BRUSH); } break; case WM_DRAWITEM: if((wp>=10 && wp<=13) || (wp>=18 && wp<=26) || wp==34 || wp==35) { 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)); return (LRESULT)GetStockObject(WHITE_BRUSH); case WM_ERASEBKGND: return 1; case WM_SETCURSOR: if ((HWND)wp==w && LOWORD(lp)==HTCLIENT) { captureCursor(w,*c); return TRUE; } break; case WM_COMMAND: if(LOWORD(wp)==40) { if(c->inlineText && HIWORD(wp)==EN_CHANGE) { readInlineText(*c); positionInlineText(*c); } if(c->inlineText && HIWORD(wp)==EN_KILLFOCUS) PostMessageW(w,FinishTextMessage,0,lp); return 0; } if (c->dragging) return 0; if(LOWORD(wp)==35) { finishInlineText(w,*c,true); if(c->selected>=0 && c->marks[c->selected].isLine()) beginMarkText(w,*c,c->selected); return 0; } if(LOWORD(wp)==34) { finishInlineText(w,*c,true); if(c->selected>=0 && (c->marks[c->selected].isText() || !c->marks[c->selected].label.empty())) { auto draft=c->marks[c->selected]; if(chooseTextFont(w,draft)) { c->remember(c->marks); c->marks[c->selected]=draft; c->textDefaults=draft.isLine()?screenshotArrowLabel(draft):draft; } } else chooseTextFont(w,c->textDefaults); SetFocus(w); InvalidateRect(w,nullptr,FALSE); return 0; } finishInlineText(w,*c,true); // Combo focus/open/close notifications are not toolbar button clicks. // Returning focus to the canvas here closes a just-opened dropdown. if((LOWORD(wp)==30 || LOWORD(wp)==31) && HIWORD(wp)!=CBN_SELCHANGE) { if(HIWORD(wp)==CBN_CLOSEUP) SetFocus(w); return 0; } if((LOWORD(wp)==30 || LOWORD(wp)==31) && HIWORD(wp)==CBN_SELCHANGE) { int value=int(SendDlgItemMessageW(w,LOWORD(wp),CB_GETCURSEL,0,0)); if(value<0 || value>3) return 0; bool selected=selectedLine(*c); if(LOWORD(wp)==30) { c->lineStyle=LineStyle(value); if(selected && c->marks[c->selected].style!=c->lineStyle) { c->remember(c->marks); c->marks[c->selected].style=c->lineStyle; } } else { auto ends=LineEnds(value); if(selected?c->marks[c->selected].freehand:c->freehand) c->freehandEnds=ends; else c->arrowEnds=ends; if(selected && c->marks[c->selected].ends!=ends) { c->remember(c->marks); c->marks[c->selected].ends=ends; } } InvalidateRect(w,nullptr,FALSE); return 0; } if(LOWORD(wp)==20) { bool selected=c->selected>=0; CHOOSECOLORW choose{sizeof(choose)}; choose.hwndOwner=w; choose.lpCustColors=c->customColors; choose.rgbResult=selected?c->marks[c->selected].color:c->currentColor; choose.Flags=CC_FULLOPEN|CC_RGBINIT; if(ChooseColorW(&choose)) { c->currentColor=choose.rgbResult; if(selected && c->marks[c->selected].color!=choose.rgbResult) { c->remember(c->marks); c->marks[c->selected].color=choose.rgbResult; } InvalidateRect(GetDlgItem(w,20),nullptr,FALSE); } } if((LOWORD(wp)==15 || LOWORD(wp)==16) && c->selected>=0 && !c->marks[c->selected].number) { auto &mark=c->marks[c->selected]; int &value=mark.isText()?mark.fontSize:mark.thickness; int next=std::clamp(value+(LOWORD(wp)==16?1:-1),mark.isText()?6:mark.isHighlight()?4:1,mark.isText()?200:mark.isHighlight()?100:30); if(next!=value) { c->remember(c->marks); value=next; } } if (LOWORD(wp)==13) { copy(w,*c); return 0; } if (LOWORD(wp)==12 || LOWORD(wp)==18) { bool changed=LOWORD(wp)==12?c->history.undo(c->marks):c->history.redo(c->marks); 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)>=21 && LOWORD(wp)<=26)) { c->arrow = LOWORD(wp)==11 || (LOWORD(wp)>=22 && LOWORD(wp)<=25); c->straight=LOWORD(wp)==22; c->textTool=LOWORD(wp)==26; c->shape=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; SendMessageW(GetDlgItem(w,10),BM_SETCHECK,c->arrow?BST_UNCHECKED:BST_CHECKED,0); SendMessageW(GetDlgItem(w,11),BM_SETCHECK,c->arrow?BST_CHECKED:BST_UNCHECKED,0); InvalidateRect(GetDlgItem(w,10),nullptr,FALSE); InvalidateRect(GetDlgItem(w,11),nullptr,FALSE); InvalidateRect(GetDlgItem(w,19),nullptr,FALSE); InvalidateRect(GetDlgItem(w,21),nullptr,FALSE); for(int id=22;id<=26;++id) InvalidateRect(GetDlgItem(w,id),nullptr,FALSE); } SetFocus(w); InvalidateRect(w,nullptr,FALSE); return 0; case WM_KEYDOWN: if (wp==VK_ESCAPE) { if(!c->editing) DestroyWindow(w); // Cancel region selection only. return 0; } if (c->editing && !c->dragging) { if(wp>=VK_LEFT && wp<=VK_DOWN && c->selected>=0 && !c->inlineText) { POINT delta{wp==VK_LEFT?-1L:wp==VK_RIGHT?1L:0L,wp==VK_UP?-1L:wp==VK_DOWN?1L:0L}; const auto &original=c->marks[c->selected]; auto part=c->selectedLabel && original.isLine() && !original.label.empty()?MarkPart::Label:MarkPart::Body; auto moved=dragScreenshotMark(original,part,{0,0},delta,c->image->width,c->image->height); if(moved.from.x!=original.from.x || moved.from.y!=original.from.y || moved.labelOffset.x!=original.labelOffset.x || moved.labelOffset.y!=original.labelOffset.y) { c->remember(c->marks); c->marks[c->selected]=std::move(moved); resolveScreenshotLinks(c->marks); InvalidateRect(w,nullptr,FALSE); } return 0; } if(wp==VK_F2 && c->selected>=0 && (c->marks[c->selected].isText() || c->marks[c->selected].isLine())) { beginMarkText(w,*c,c->selected); return 0; } if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='C') { copy(w,*c); return 0; } if ((GetKeyState(VK_CONTROL)&0x8000) && (wp=='Z' || wp=='Y')) SendMessageW(w,WM_COMMAND,wp=='Y'||(GetKeyState(VK_SHIFT)&0x8000)?18:12,0); if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='D' && c->selected>=0) { auto duplicate=cloneScreenshotMark(c->marks[c->selected],c->marks,c->image->width,c->image->height); c->remember(c->marks); c->marks.push_back(duplicate); c->selected=int(c->marks.size())-1; c->selectedLabel=false; InvalidateRect(w,nullptr,FALSE); } if(!(GetKeyState(VK_CONTROL)&0x8000)) { if(wp=='T') SendMessageW(w,WM_COMMAND,26,0); if(wp=='L') SendMessageW(w,WM_COMMAND,22,0); if(wp=='R') SendMessageW(w,WM_COMMAND,23,0); if(wp=='O') SendMessageW(w,WM_COMMAND,24,0); if(wp=='E') SendMessageW(w,WM_COMMAND,25,0); } if (wp=='N' || wp=='A') SendMessageW(w,WM_COMMAND,wp=='N'?10:11,0); if(wp=='H') SendMessageW(w,WM_COMMAND,19,0); if(wp=='F' && !(GetKeyState(VK_CONTROL)&0x8000)) SendMessageW(w,WM_COMMAND,21,0); if (wp==VK_OEM_MINUS || wp==VK_SUBTRACT) SendMessageW(w,WM_COMMAND,15,0); if (wp==VK_OEM_PLUS || wp==VK_ADD) SendMessageW(w,WM_COMMAND,16,0); if (wp==VK_RETURN) { copy(w,*c); return 0; } if (wp==VK_DELETE && c->selected>=0) { c->remember(c->marks); c->marks.erase(c->marks.begin()+c->selected); resolveScreenshotLinks(c->marks); c->selected=-1; c->selectedLabel=false; InvalidateRect(w,nullptr,FALSE); } } return 0; case WM_LBUTTONDBLCLK: if(c->editing && !c->dragging) { POINT p{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)}; if(PtInRect(&c->view,p)) { auto [index,part]=markAt(*c,imagePoint(*c,p)); if(index>=0 && (c->marks[index].isText() || c->marks[index].isLine())) { c->selected=index; beginMarkText(w,*c,index); } } } return 0; case WM_LBUTTONDOWN: { finishInlineText(w,*c,true); POINT p{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)}; auto hitPart=c->editing?markAt(*c,imagePoint(*c,p,false)).second:MarkPart::None; bool thickness=hitPart==MarkPart::Thickness || hitPart==MarkPart::Rotation; if (c->editing && !PtInRect(&c->view,p) && !thickness) { c->selected=-1; c->selectedLabel=false; InvalidateRect(w,nullptr,FALSE); return 0; } SetFocus(w); if (c->editing) p=imagePoint(*c,p,!thickness); c->start=c->end=p; c->part=MarkPart::None; if (c->editing) { auto [index,part]=markAt(*c,p); if(index>=0) { c->selected=index; c->selectedLabel=part==MarkPart::Label || (part==MarkPart::Rotation && c->marks[index].isLine()); c->part=part; c->original=c->marks[index]; c->beforeDrag=c->marks; c->dragging=true; SetCapture(w); captureCursor(w,*c); InvalidateRect(w,nullptr,FALSE); return 0; } c->selected=-1; c->selectedLabel=false; } if(c->editing && c->textTool) { Mark draft=c->textDefaults; draft.text.clear(); draft.from=draft.to=p; draft.color=c->currentColor; beginInlineText(w,*c,draft,-1); return 0; } if (c->editing && !c->arrow && !c->highlight && !c->freehand) { int number=nextScreenshotNumber(c->marks); c->remember(c->marks); c->marks.push_back({p,p,number}); c->marks.back().color=c->currentColor; c->selected=int(c->marks.size())-1; c->selectedLabel=false; } else { if(c->editing && (c->highlight || c->freehand)) { c->stroke={p,p,0}; c->stroke.thickness=c->highlight?24:3; c->stroke.color=c->currentColor; c->stroke.freehand=c->freehand; c->stroke.style=c->lineStyle; c->stroke.ends=c->freehandEnds; c->stroke.points.push_back(p); } c->dragging=true; SetCapture(w); } InvalidateRect(w,nullptr,FALSE); return 0; } case WM_MOUSEWHEEL: { if(!c->editing || c->dragging) return 0; POINT p{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)}; ScreenToClient(w,&p); if(!PtInRect(&c->view,p)) return 0; auto [index,part]=markAt(*c,imagePoint(*c,p)); if(index<0) { c->wheelDelta=0; c->wheelTarget=-1; return 0; } if(c->wheelTarget!=index) { c->wheelDelta=0; c->wheelTarget=index; } c->wheelDelta+=GET_WHEEL_DELTA_WPARAM(wp); int steps=c->wheelDelta/WHEEL_DELTA; c->wheelDelta%=WHEEL_DELTA; if(!steps) return 0; auto &mark=c->marks[index]; bool textSize=mark.isText() || part==MarkPart::Label; int &value=textSize?mark.fontSize:mark.number?mark.radius:mark.thickness; int next=std::clamp(value+steps,textSize?6:mark.number?8:mark.isHighlight()?4:1,textSize?200:mark.number?200:mark.isHighlight()?100:30); if(next!=value) { c->remember(c->marks); value=next; c->selected=index; resolveScreenshotLinks(c->marks); InvalidateRect(w,nullptr,FALSE); } return 0; } case WM_MOUSEMOVE: case WM_LBUTTONUP: if (c->dragging) { POINT p{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)}; c->end=c->editing?imagePoint(*c,p,c->part!=MarkPart::Thickness && c->part!=MarkPart::Rotation):POINT{std::clamp(p.x,0L,LONG(c->image->width)),std::clamp(p.y,0L,LONG(c->image->height))}; if(c->editing && c->part==MarkPart::None && c->shape!=ScreenshotShape::None) c->end=constrainScreenshotShape(c->shape,c->start,c->end,c->image->width,c->image->height); if(c->editing && (c->highlight || c->freehand) && c->part==MarkPart::None) { auto last=c->stroke.points.back(); if(last.x!=c->end.x || last.y!=c->end.y) c->stroke.points.push_back(c->end); c->stroke.to=c->end; } if(c->editing && c->part!=MarkPart::None) { c->marks[c->selected]=dragScreenshotMark(c->original,c->part,c->start,c->end,c->image->width,c->image->height,(GetKeyState(VK_SHIFT)&0x8000)!=0); resolveScreenshotLinks(c->marks); } if (msg==WM_LBUTTONUP) { c->dragging=false; ReleaseCapture(); if (!c->editing) { finishSelection(w,*c); return 0; } else if(c->part!=MarkPart::None) { if(c->part==MarkPart::Start || c->part==MarkPart::End) { auto &mark=c->marks[c->selected]; linkScreenshotEndpoint(mark,c->part==MarkPart::Start,c->end,c->marks,6); resolveScreenshotLinks(c->marks); } auto &changed=c->marks[c->selected]; if(c->start.x!=c->end.x || c->start.y!=c->end.y || changed.fromNumber!=c->original.fromNumber || changed.toNumber!=c->original.toNumber) c->remember(c->beforeDrag); c->beforeDrag.clear(); c->part=MarkPart::None; } else if(c->highlight || c->freehand) { c->remember(c->marks); c->marks.push_back(std::move(c->stroke)); if(c->freehand) { auto &mark=c->marks.back(); linkScreenshotEndpoint(mark,true,c->start,c->marks,6); linkScreenshotEndpoint(mark,false,c->end,c->marks,6); resolveScreenshotLinks(c->marks); } c->selected=int(c->marks.size())-1; c->selectedLabel=false; } else if (std::hypot(double(c->end.x-c->start.x),double(c->end.y-c->start.y))>=4) { c->remember(c->marks); c->marks.push_back({c->start,c->end,0}); c->marks.back().color=c->currentColor; 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(); linkScreenshotEndpoint(mark,true,c->start,c->marks,6); linkScreenshotEndpoint(mark,false,c->end,c->marks,6); resolveScreenshotLinks(c->marks); c->selected=int(c->marks.size())-1; c->selectedLabel=false; } } InvalidateRect(w,nullptr,FALSE); } captureCursor(w,*c); return 0; case WM_CAPTURECHANGED: if(c->dragging && c->part!=MarkPart::None) { c->marks=std::move(c->beforeDrag); c->part=MarkPart::None; } c->dragging=false; InvalidateRect(w,nullptr,FALSE); return 0; case WM_GETMINMAXINFO: if (c->editing) { auto limits = reinterpret_cast(lp); limits->ptMinTrackSize = {MulDiv(650,GetDpiForWindow(w),96),MulDiv(240,GetDpiForWindow(w),96)}; return 0; } break; case WM_SIZE: layout(w,*c); positionInlineText(*c); return 0; case WM_DPICHANGED: if (c->editing) { auto r=reinterpret_cast(lp); SetWindowPos(w,nullptr,r->left,r->top,r->right-r->left,r->bottom-r->top,SWP_NOZORDER); layout(w,*c); positionInlineText(*c); } return 0; case WM_DISPLAYCHANGE: if (!c->editing) DestroyWindow(w); return 0; case WM_PAINT: { PAINTSTRUCT ps; auto dc=BeginPaint(w,&ps); // Always balance BeginPaint, including allocation failure. try { RECT client; GetClientRect(w,&client); if (!c->editing) { // Compose the entire frame off screen. Never expose a cleared desktop // or a partially drawn selection between mouse moves. auto buffer=c->frame->dc; BitBlt(buffer,0,0,c->image->width,c->image->height,c->dimmed->dc,0,0,SRCCOPY); if (c->dragging) { auto r=region(c->start,c->end); BitBlt(buffer,r.left,r.top,r.right-r.left,r.bottom-r.top,c->image->dc,r.left,r.top,SRCCOPY); FrameRect(buffer,&r,static_cast(GetStockObject(WHITE_BRUSH))); } BitBlt(dc,0,0,c->image->width,c->image->height,buffer,0,0,SRCCOPY); } else { int width=std::max(1L,client.right),height=std::max(1L,client.bottom); if (!c->frame || c->frame->width!=width || c->frame->height!=height) c->frame=std::make_unique(width,height); auto buffer=c->frame->dc; FillRect(buffer,&client,GetSysColorBrush(COLOR_APPWORKSPACE)); RECT toolbar{0,0,client.right,c->panelHeight}; FillRect(buffer,&toolbar,static_cast(GetStockObject(WHITE_BRUSH))); auto divider=CreatePen(PS_SOLID,1,RGB(226,232,240)); auto previous=SelectObject(buffer,divider); MoveToEx(buffer,0,c->panelHeight-1,nullptr); LineTo(buffer,client.right,c->panelHeight-1); int dpi=GetDpiForWindow(w); for(int x:{368,452}) { MoveToEx(buffer,MulDiv(x,dpi,96),MulDiv(13,dpi,96),nullptr); LineTo(buffer,MulDiv(x,dpi,96),MulDiv(31,dpi,96)); } SelectObject(buffer,previous); DeleteObject(divider); Surface rendered(c->image->width,c->image->height); BitBlt(rendered.dc,0,0,rendered.width,rendered.height,c->image->dc,0,0,SRCCOPY); Mark preview{c->start,c->end,0}; preview.color=c->currentColor; preview.style=c->lineStyle; preview.ends=c->straight || c->shape!=ScreenshotShape::None?LineEnds::None:c->arrowEnds; preview.shape=c->shape; auto visibleMarks=c->marks; if(c->inlineText && c->textIndex>=0) { if(c->editingLabel) visibleMarks[c->textIndex].label.clear(); else visibleMarks.erase(visibleMarks.begin()+c->textIndex); } drawMarks(rendered.dc,visibleMarks,c->dragging && c->part==MarkPart::None?((c->highlight || c->freehand)?&c->stroke:&preview):nullptr); SetStretchBltMode(buffer,HALFTONE); SetBrushOrgEx(buffer,0,0,nullptr); StretchBlt(buffer,c->view.left,c->view.top,c->view.right-c->view.left,c->view.bottom-c->view.top, rendered.dc,0,0,rendered.width,rendered.height,SRCCOPY); drawHandles(buffer,*c); BitBlt(dc,0,0,width,height,buffer,0,0,SRCCOPY); } } catch (...) { EndPaint(w,&ps); throw; } EndPaint(w,&ps); if(c->editing) { InvalidateRect(GetDlgItem(w,20),nullptr,FALSE); syncLineOptions(w,*c); updateStatus(*c); } return 0; } case WM_NCDESTROY: if (activeCapture == w) activeCapture=nullptr; SetWindowLongPtrW(w,GWLP_USERDATA,0); if (c->owned) { auto &host=c->app; if (--host.windows==0) PostMessageW(host.window,ScreenshotIdle,0,0); delete c; } return DefWindowProcW(w,msg,wp,lp); } } catch (const std::exception &e) { MessageBoxW(w,wide(e.what()).c_str(),L"Screen capture",MB_OK|MB_ICONERROR); } return DefWindowProcW(w,msg,wp,lp); } } // namespace RECT screenshotTextBounds(const ScreenshotMark &m) { auto r=plainTextBounds(m); if(m.rotation==0) return r; POINT corners[]{rotateTextPoint(m,{r.left,r.top}),rotateTextPoint(m,{r.right,r.top}), rotateTextPoint(m,{r.right,r.bottom}),rotateTextPoint(m,{r.left,r.bottom})}; RECT bounds{corners[0].x,corners[0].y,corners[0].x,corners[0].y}; for(auto p:corners) { bounds.left=std::min(bounds.left,p.x); bounds.top=std::min(bounds.top,p.y); bounds.right=std::max(bounds.right,p.x); bounds.bottom=std::max(bounds.bottom,p.y); } return bounds; } POINT screenshotTextRotationHandle(const ScreenshotMark &m,double clearance) { auto r=plainTextBounds(m); return rotateTextPoint(m,{(r.left+r.right)/2,r.top-LONG(std::lround(clearance))}); } ScreenshotMark screenshotArrowLabel(const ScreenshotMark &m) { ScreenshotMark label{{0,0},{0,0},0}; label.text=m.label; label.color=m.color; label.fontName=m.fontName; label.rotation=m.rotation; label.fontSize=m.fontSize; label.fontWeight=m.fontWeight; label.fontItalic=m.fontItalic; auto bounds=plainTextBounds(label); auto [from,to]=thicknessSegment(m); double angle=std::atan2(double(to.y-from.y),double(to.x-from.x)); double cs=std::cos(angle),sn=std::sin(angle); if(!m.labelRotationManual) { label.rotation=angle*180/3.141592653589793; if(label.rotation>90) label.rotation-=180; else if(label.rotation< -90) label.rotation+=180; } label.from={(from.x+to.x)/2+LONG(std::lround(m.labelOffset.x*cs-m.labelOffset.y*sn))-(bounds.right-bounds.left)/2, (from.y+to.y)/2+LONG(std::lround(m.labelOffset.x*sn+m.labelOffset.y*cs))-(bounds.bottom-bounds.top)/2}; label.to=label.from; return label; } void ScreenshotHistory::remember(const std::vector &marks) { if(past.size()==100) past.pop_front(); past.push_back(marks); future.clear(); } bool ScreenshotHistory::undo(std::vector &marks) { if(past.empty()) return false; future.push_back(marks); marks=std::move(past.back()); past.pop_back(); return true; } bool ScreenshotHistory::redo(std::vector &marks) { if(future.empty()) return false; past.push_back(marks); marks=std::move(future.back()); future.pop_back(); return true; } POINT screenshotThicknessHandle(const ScreenshotMark &m, double clearance) { auto [from,to]=thicknessSegment(m); double dx=to.x-from.x,dy=to.y-from.y,length=std::hypot(dx,dy); if(length<0.001) { dx=1; dy=0; length=1; } double offset=clearance+m.thickness/2.0; return {LONG(std::lround((from.x+to.x)/2.0-dy/length*offset)), LONG(std::lround((from.y+to.y)/2.0+dx/length*offset))}; } int nextScreenshotNumber(const std::vector &marks) { // With N objects, the smallest free positive number cannot exceed N+1. std::vector used(marks.size()+2,false); for(const auto &m:marks) if(m.number>0 && size_t(m.number) &marks,int width,int height) { auto result=original; result.fromNumber=result.toNumber=0; if(result.number) result.number=nextScreenshotNumber(marks); auto bounds=markBounds(result); LONG dx=bounds.right+16 &marks, double tolerance) { auto &link=start?arrow.fromNumber:arrow.toNumber; link=0; if(arrow.shape!=ScreenshotShape::None || arrow.isText()) return; for(auto it=marks.rbegin();it!=marks.rend();++it) if(it->number && std::hypot(double(drop.x-it->from.x),double(drop.y-it->from.y))<=it->radius+tolerance) { link=it->number; break; } } void resolveScreenshotLinks(std::vector &marks) { auto badge=[&](int number) -> const ScreenshotMark * { if(number) for(const auto &m:marks) if(m.number==number) return &m; return nullptr; }; auto rim=[](const ScreenshotMark &m, POINT toward, bool start) -> POINT { double dx=toward.x-m.from.x,dy=toward.y-m.from.y,length=std::hypot(dx,dy); if(length<0.001) { dx=start?1:-1; dy=0; length=1; } return {m.from.x+LONG(std::lround(m.radius*dx/length)),m.from.y+LONG(std::lround(m.radius*dy/length))}; }; for(auto &m:marks) { if(m.number || m.isHighlight() || m.isText() || m.shape!=ScreenshotShape::None) continue; auto from=badge(m.fromNumber),to=badge(m.toNumber); if(!from) m.fromNumber=0; if(!to) m.toNumber=0; POINT start=from?from->from:m.from, end=to?to->from:m.to; if(from) m.from=rim(*from,end,true); if(to) m.to=rim(*to,start,false); if(m.freehand && !m.points.empty()) { m.points.front()=m.from; m.points.back()=m.to; } } } MarkPart hitScreenshotMark(const ScreenshotMark &m, POINT p, double tolerance) { if(m.isText()) { auto r=plainTextBounds(m); auto local=rotateTextPoint(m,p,-1); return PtInRect(&r,local)?MarkPart::Body:MarkPart::None; } if(m.isLine() && !m.label.empty() && hitScreenshotMark(screenshotArrowLabel(m),p,tolerance)==MarkPart::Body) return MarkPart::Label; auto distance=[](POINT a, POINT b) { return std::hypot(double(a.x-b.x),double(a.y-b.y)); }; if(!m.points.empty()) { if(m.freehand && distance(p,m.from)<=tolerance) return MarkPart::Start; if(m.freehand && distance(p,m.to)<=tolerance) return MarkPart::End; double reach=tolerance+std::clamp(m.thickness,m.isHighlight()?4:1,m.isHighlight()?100:30)/2.0; POINT from=m.points.front(); for(auto to:m.points) { double dx=to.x-from.x,dy=to.y-from.y,length=dx*dx+dy*dy; double t=length?std::clamp(((p.x-from.x)*dx+(p.y-from.y)*dy)/length,0.0,1.0):0; if(std::hypot(p.x-from.x-t*dx,p.y-from.y-t*dy)<=reach) return MarkPart::Body; from=to; } return MarkPart::None; } if(m.number) { if(distance(p,{m.from.x+m.radius,m.from.y})<=std::min(tolerance,double(m.radius)/3)) return MarkPart::Radius; return distance(p,m.from)<=m.radius ? MarkPart::Body : MarkPart::None; } if(distance(p,m.from)<=tolerance) return MarkPart::Start; if(distance(p,m.to)<=tolerance) return MarkPart::End; if(m.shape!=ScreenshotShape::None) { auto r=region(m.from,m.to); double reach=tolerance+std::clamp(m.thickness,1,30)/2.0; if(m.shape==ScreenshotShape::Rectangle) { bool vertical=p.y>=r.top-reach && p.y<=r.bottom+reach && std::min(std::abs(p.x-r.left),std::abs(p.x-r.right))<=reach; bool horizontal=p.x>=r.left-reach && p.x<=r.right+reach && std::min(std::abs(p.y-r.top),std::abs(p.y-r.bottom))<=reach; return vertical || horizontal?MarkPart::Body:MarkPart::None; } double rx=(r.right-r.left)/2.0,ry=(r.bottom-r.top)/2.0; if(rx<1 || ry<1) return MarkPart::None; double x=p.x-(r.left+r.right)/2.0,y=p.y-(r.top+r.bottom)/2.0; double outer=x*x/((rx+reach)*(rx+reach))+y*y/((ry+reach)*(ry+reach)); double inner=rx<=reach || ry<=reach?2:x*x/((rx-reach)*(rx-reach))+y*y/((ry-reach)*(ry-reach)); return outer<=1 && inner>=1?MarkPart::Body:MarkPart::None; } double dx=m.to.x-m.from.x,dy=m.to.y-m.from.y,length=dx*dx+dy*dy; if(length==0) return MarkPart::None; double t=std::clamp(((p.x-m.from.x)*dx+(p.y-m.from.y)*dy)/length,0.0,1.0); return std::hypot(p.x-m.from.x-t*dx,p.y-m.from.y-t*dy)<=tolerance+std::clamp(m.thickness,1,30)/2.0 ? MarkPart::Body : MarkPart::None; } POINT constrainScreenshotShape(ScreenshotShape shape, POINT anchor, POINT position, int width, int height) { if(shape!=ScreenshotShape::Circle) return position; LONG dx=position.x-anchor.x,dy=position.y-anchor.y; LONG size=std::max(std::abs(dx),std::abs(dy)); size=std::min({size,dx<0?anchor.x:LONG(width-1)-anchor.x,dy<0?anchor.y:LONG(height-1)-anchor.y}); return {anchor.x+(dx<0?-size:size),anchor.y+(dy<0?-size:size)}; } ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part, POINT origin, POINT position, int width, int height, bool snapRotation) { auto m=original; if(part==MarkPart::Rotation && (m.isText() || (m.isLine() && !m.label.empty()))) { if(position.x==origin.x && position.y==origin.y) return m; auto text=m.isText()?m:screenshotArrowLabel(m); auto r=plainTextBounds(text); double cx=(r.left+r.right)/2.0,cy=(r.top+r.bottom)/2.0; double a=std::atan2(origin.y-cy,origin.x-cx),b=std::atan2(position.y-cy,position.x-cx); m.rotation=text.rotation+(b-a)*180/3.141592653589793; if(snapRotation) m.rotation=std::round(m.rotation/15)*15; m.rotation=std::remainder(m.rotation,360); if(m.isLine()) m.labelRotationManual=true; } else if(part==MarkPart::Label) { auto [from,to]=thicknessSegment(m); double angle=std::atan2(double(to.y-from.y),double(to.x-from.x)); double dx=position.x-origin.x,dy=position.y-origin.y; m.labelOffset.x+=dx*std::cos(angle)+dy*std::sin(angle); m.labelOffset.y+=-dx*std::sin(angle)+dy*std::cos(angle); } else if(part==MarkPart::Thickness) { auto [from,to]=thicknessSegment(m); double dx=to.x-from.x,dy=to.y-from.y,length=std::hypot(dx,dy); if(length<0.001) { dx=1; dy=0; length=1; } double change=(-(position.x-origin.x)*dy+(position.y-origin.y)*dx)/length; m.thickness=std::clamp(int(std::lround(m.thickness+2*change)),1,30); } else if(part==MarkPart::Radius) { double change=std::hypot(double(position.x-m.from.x),double(position.y-m.from.y))- std::hypot(double(origin.x-m.from.x),double(origin.y-m.from.y)); m.radius=std::clamp(int(std::lround(m.radius+change)),8,200); } else if(part==MarkPart::Body) { m.fromNumber=m.toNumber=0; auto shift=[&](LONG delta,LONG first,LONG last,int extent) { LONG low=-first,high=LONG(extent-1)-last; if(low>high && m.isText()) { // Oversized text cannot fit entirely inside the image. Keep a // small selectable strip visible instead of freezing this axis. LONG visible=std::min(16,std::max(1,extent)); low=visible-last; high=LONG(extent)-visible-first; } return low<=high?std::clamp(delta,low,high):0L; }; auto bounds=markBounds(m); LONG dx=shift(position.x-origin.x,bounds.left,bounds.right,width); LONG dy=shift(position.y-origin.y,bounds.top,bounds.bottom,height); m.from.x+=dx; m.to.x+=dx; m.from.y+=dy; m.to.y+=dy; for(auto &point:m.points) { point.x+=dx; point.y+=dy; } } else if(part==MarkPart::Start || part==MarkPart::End) { (part==MarkPart::Start?m.fromNumber:m.toNumber)=0; auto &endpoint=part==MarkPart::Start?m.from:m.to; endpoint.x=std::clamp(endpoint.x+position.x-origin.x,0L,LONG(width-1)); endpoint.y=std::clamp(endpoint.y+position.y-origin.y,0L,LONG(height-1)); endpoint=constrainScreenshotShape(m.shape,part==MarkPart::Start?m.to:m.from,endpoint,width,height); if(m.freehand && !m.points.empty()) (part==MarkPart::Start?m.points.front():m.points.back())=endpoint; } return m; } Clip screenshotClip(HDC source, int width, int height, const std::vector &marks) { Surface output(width,height); if (!BitBlt(output.dc,0,0,width,height,source,0,0,SRCCOPY)) throw std::runtime_error("Cannot render screen image"); drawMarks(output.dc,marks); GdiFlush(); BITMAPINFOHEADER header{sizeof(BITMAPINFOHEADER),width,-height,1,32,BI_RGB}; Bytes bytes(sizeof(header)+size_t(width)*height*4); memcpy(bytes.data(),&header,sizeof(header)); memcpy(bytes.data()+sizeof(header),output.pixels,bytes.size()-sizeof(header)); return Clip{{{CF_DIB,L"",std::move(bytes)}}}; } void showScreenshotEditor(ScreenshotHost &app,const Clip &clip) { auto pixels=decodeImage(clip); auto c=std::make_unique(app); c->image=std::make_unique(pixels.width,pixels.height); memcpy(c->image->pixels,pixels.bgra.data(),pixels.bgra.size()); c->start={0,0}; c->end={(LONG)pixels.width,(LONG)pixels.height}; c->copied=true; WNDCLASSW cls{}; cls.hInstance=app.instance; cls.lpfnWndProc=captureProc; cls.lpszClassName=L"KSwitcher.Capture"; cls.style=CS_DBLCLKS; cls.hCursor=LoadCursorW(nullptr,IDC_CROSS); cls.hIcon=LoadIconW(app.instance,MAKEINTRESOURCEW(IDI_KSWITCHER)); RegisterClassW(&cls); auto window=CreateWindowExW(0,cls.lpszClassName,L"KSwitcher Screenshot",WS_POPUP,0,0,1,1, app.window,nullptr,app.instance,c.get()); if(!window) throw std::runtime_error("Cannot open screenshot editor"); c->owned=true; ++app.windows; auto state=c.release(); edit(window,*state); } void showScreenshot(ScreenshotHost &app) { if (activeCapture) { showRequestedWindow(activeCapture); SetForegroundWindow(activeCapture); return; } auto c=std::make_unique(app); int x=GetSystemMetrics(SM_XVIRTUALSCREEN),y=GetSystemMetrics(SM_YVIRTUALSCREEN); int width=GetSystemMetrics(SM_CXVIRTUALSCREEN),height=GetSystemMetrics(SM_CYVIRTUALSCREEN); c->image=std::make_unique(width,height); auto screen=GetDC(nullptr); BOOL ok=BitBlt(c->image->dc,0,0,width,height,screen,x,y,SRCCOPY|CAPTUREBLT); ReleaseDC(nullptr,screen); if (!ok) throw std::runtime_error("Cannot capture the desktop"); c->dimmed=std::make_unique(width,height); c->frame=std::make_unique(width,height); GdiFlush(); auto source=static_cast(c->image->pixels); auto dimmed=static_cast(c->dimmed->pixels); for (size_t i=0;i(source[i]*55/100); WNDCLASSW cls{}; cls.hInstance=app.instance; cls.lpfnWndProc=captureProc; cls.lpszClassName=L"KSwitcher.Capture"; cls.style=CS_DBLCLKS; cls.hCursor=LoadCursorW(nullptr,IDC_CROSS); cls.hIcon=LoadIconW(app.instance,MAKEINTRESOURCEW(IDI_KSWITCHER)); RegisterClassW(&cls); // Transfer lifetime to WM_NCDESTROY only after creation succeeds. activeCapture=CreateWindowExW(WS_EX_TOPMOST,cls.lpszClassName,L"KSwitcher — drag to capture; Esc to cancel", WS_POPUP,x,y,width,height,app.window,nullptr,app.instance,c.get()); if (!activeCapture) throw std::runtime_error("Cannot open screen capture"); c->owned=true; ++app.windows; c.release(); showRequestedWindow(activeCapture); SetForegroundWindow(activeCapture); SetFocus(activeCapture); } } // namespace ks