1586 lines
80 KiB
C++
1586 lines
80 KiB
C++
#include "screenshot.h"
|
|
#include "ocr.h"
|
|
#include "../resources/resource.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <windowsx.h>
|
|
#include <commctrl.h>
|
|
#include <commdlg.h>
|
|
|
|
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<Surface> image;
|
|
std::unique_ptr<Surface> 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<Mark> 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<Mark> 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<Mark> &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<POINT,POINT> 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<unsigned char *>(target.dsBm.bmBits);
|
|
auto coverage=static_cast<unsigned char *>(mask.pixels);
|
|
unsigned channels[]{GetBValue(m.color),GetGValue(m.color),GetRValue(m.color)};
|
|
for(int y=0;y<mask.height;++y) for(int x=0;x<mask.width;++x)
|
|
{
|
|
if(!coverage[(size_t(y)*mask.width+x)*4]) continue;
|
|
auto at=pixels+size_t(y+r.top)*target.dsBm.bmWidthBytes+size_t(x+r.left)*4;
|
|
// Multiply at 40% opacity keeps dark text legible. A stroke is blended once,
|
|
// including where it crosses itself, so slower drawing never darkens it.
|
|
for(int channel=0;channel<3;++channel) at[channel]=static_cast<unsigned char>(at[channel]*(153+channels[channel]*102/255)/255);
|
|
}
|
|
}
|
|
void drawMarks(HDC dc, const std::vector<Mark> &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;i<count;++i) pattern[i]*=thickness;
|
|
LOGBRUSH ink{BS_SOLID,m.color,0};
|
|
auto arrowPen=ExtCreatePen(PS_GEOMETRIC|PS_ENDCAP_ROUND|PS_JOIN_ROUND|(count?PS_USERSTYLE:PS_SOLID),
|
|
thickness,&ink,count,count?pattern:nullptr);
|
|
auto previousPen=SelectObject(dc,arrowPen);
|
|
if(m.shape!=ScreenshotShape::None) {
|
|
auto hollow=SelectObject(dc,GetStockObject(HOLLOW_BRUSH));
|
|
auto r=region(m.from,m.to);
|
|
if(m.shape==ScreenshotShape::Rectangle) Rectangle(dc,r.left,r.top,r.right+1,r.bottom+1);
|
|
else Ellipse(dc,r.left,r.top,r.right+1,r.bottom+1);
|
|
SelectObject(dc,hollow);
|
|
SelectObject(dc,previousPen); DeleteObject(arrowPen);
|
|
} else {
|
|
if(m.freehand && m.points.size()>1) 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<int,MarkPart> 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<HBRUSH>(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<Capture *>(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<Surface>(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<std::size(ToolIds);++i)
|
|
{
|
|
auto button = CreateWindowW(L"BUTTON",labels[i],WS_CHILD|WS_VISIBLE|BS_OWNERDRAW,
|
|
0,0,0,0,w,(HMENU)(INT_PTR)ToolIds[i],c.app.instance,nullptr);
|
|
SendMessageW(button, WM_SETFONT, (WPARAM)GetStockObject(DEFAULT_GUI_FONT), TRUE);
|
|
SetWindowSubclass(button,toolHover,1,0);
|
|
TOOLINFOW tool{sizeof(tool)};
|
|
tool.uFlags=TTF_IDISHWND|TTF_SUBCLASS; tool.hwnd=w; tool.uId=(UINT_PTR)button;
|
|
tool.lpszText=const_cast<wchar_t *>(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<wchar_t *>(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<wchar_t *>(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<Capture *>(GetWindowLongPtrW(w,GWLP_USERDATA));
|
|
if (msg == WM_NCCREATE)
|
|
{
|
|
c = static_cast<Capture *>(reinterpret_cast<CREATESTRUCTW *>(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<DRAWITEMSTRUCT *>(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<MINMAXINFO *>(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<RECT *>(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<HBRUSH>(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<Surface>(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<HBRUSH>(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<ScreenshotMark> &marks)
|
|
{
|
|
if(past.size()==100) past.pop_front();
|
|
past.push_back(marks);
|
|
future.clear();
|
|
}
|
|
bool ScreenshotHistory::undo(std::vector<ScreenshotMark> &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<ScreenshotMark> &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<ScreenshotMark> &marks)
|
|
{
|
|
// With N objects, the smallest free positive number cannot exceed N+1.
|
|
std::vector<bool> used(marks.size()+2,false);
|
|
for(const auto &m:marks)
|
|
if(m.number>0 && size_t(m.number)<used.size()) used[m.number]=true;
|
|
for(size_t number=1;number<used.size();++number)
|
|
if(!used[number]) return int(number);
|
|
return 1;
|
|
}
|
|
ScreenshotMark cloneScreenshotMark(const ScreenshotMark &original,const std::vector<ScreenshotMark> &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<width?16:-16;
|
|
LONG dy=bounds.bottom+16<height?16:-16;
|
|
return dragScreenshotMark(result,MarkPart::Body,{0,0},{dx,dy},width,height);
|
|
}
|
|
void linkScreenshotEndpoint(ScreenshotMark &arrow, bool start, POINT drop,
|
|
const std::vector<ScreenshotMark> &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<ScreenshotMark> &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<ScreenshotMark> &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<Capture>(app);
|
|
c->image=std::make_unique<Surface>(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<Capture>(app);
|
|
int x=GetSystemMetrics(SM_XVIRTUALSCREEN),y=GetSystemMetrics(SM_YVIRTUALSCREEN);
|
|
int width=GetSystemMetrics(SM_CXVIRTUALSCREEN),height=GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
|
c->image=std::make_unique<Surface>(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<Surface>(width,height);
|
|
c->frame=std::make_unique<Surface>(width,height);
|
|
GdiFlush();
|
|
auto source=static_cast<const unsigned char *>(c->image->pixels);
|
|
auto dimmed=static_cast<unsigned char *>(c->dimmed->pixels);
|
|
for (size_t i=0;i<size_t(width)*height*4;++i)
|
|
dimmed[i]=(i%4==3)?source[i]:static_cast<unsigned char>(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
|