Release v0.6.17: constrained pencil strokes and snippet editor states

This commit is contained in:
2026-10-30 14:13:52 +02:00
parent a830f55575
commit 4eb9f6c671
11 changed files with 227 additions and 28 deletions
+75 -2
View File
@@ -178,6 +178,11 @@ struct Capture
COLORREF currentColor=RGB(225,42,50);
COLORREF customColors[16]{};
Mark stroke{};
bool strokeConstrained{};
size_t strokeAnchor{};
int strokeAxis{}; // 0 = not chosen, 1 = horizontal, 2 = vertical, 3/4 = diagonals.
bool strokeAxisSettled{};
std::vector<POINT> strokeMotion;
POINT start{}, end{};
RECT selection{}, view{};
std::vector<Mark> marks;
@@ -250,7 +255,7 @@ void updateStatus(Capture &c)
else if(c.pointer) text=L"Pointer (V): select and edit objects | Drag handles = geometry or thickness | Empty space clears selection";
else if(c.textTool) text=L"Text (T): click the image to type | Double-click existing text to edit";
else if(c.highlight) text=L"Highlight (H): drag to paint | Ctrl+drag = paint over an object | V = pointer";
else if(c.freehand) text=L"Freehand (F): drag to draw | Ctrl+drag = draw over an object | V = pointer";
else if(c.freehand) text=L"Freehand (F): Shift = 45-degree steps; move >50 px off line to turn | Ctrl = draw over objects";
else if(c.arrow) text=L"Drag to draw | Ctrl+drag over objects = linked connector | V = pointer";
else text=L"Number (N): click to add | V = select and edit | Ctrl+C = copy screenshot";
if(text!=c.statusText) {
@@ -841,7 +846,7 @@ void edit(HWND w, Capture &c)
InitCommonControlsEx(&statusControls);
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
0,0,0,0,w,(HMENU)50,c.app.instance,nullptr);
const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Ctrl+drag to draw over an object", L"Highlight (H); Ctrl+drag to paint over an object",
const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Shift for 45-degree steps; move >50 px off line to turn; Ctrl to draw over objects", L"Highlight (H); Ctrl+drag to paint over an object",
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
static_assert(std::size(labels)==std::size(ToolIds));
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
@@ -1045,6 +1050,13 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
SetFocus(w); captureCursor(w,*c); InvalidateRect(w,nullptr,FALSE); return 0;
case WM_KEYDOWN:
if(wp==VK_SHIFT && c->editing && c->dragging && c->freehand && c->part==MarkPart::None && !c->strokeConstrained) {
c->strokeConstrained=true;
c->strokeAnchor=c->stroke.points.size()-1;
c->strokeAxis=0;
c->strokeAxisSettled=false;
c->strokeMotion={c->stroke.points.back()};
}
if (wp==VK_ESCAPE) {
if(!c->editing) DestroyWindow(w); // Cancel region selection only.
return 0;
@@ -1100,6 +1112,9 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
}
}
return 0;
case WM_KEYUP:
if(wp==VK_SHIFT) c->strokeConstrained=false;
return 0;
case WM_LBUTTONDBLCLK:
if(c->editing && !c->dragging) {
POINT p{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)};
@@ -1156,6 +1171,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
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->strokeConstrained=false;
}
c->dragging=true; SetCapture(w);
}
@@ -1193,6 +1209,63 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
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) {
bool constrained=c->freehand && ((wp&MK_SHIFT) || (GetKeyState(VK_SHIFT)&0x8000));
if(constrained) {
if(!c->strokeConstrained) {
c->strokeAnchor=c->stroke.points.size()-1;
c->strokeAxis=0;
c->strokeAxisSettled=false;
c->strokeMotion={c->stroke.points.back()};
}
auto anchor=c->stroke.points[c->strokeAnchor];
// Measure the actual mouse trajectory, not its snapped projection.
auto raw=c->end;
if(c->strokeMotion.back().x!=raw.x || c->strokeMotion.back().y!=raw.y)
c->strokeMotion.push_back(raw);
while(c->strokeMotion.size()>2 && (c->strokeMotion.size()>64 ||
std::hypot(double(raw.x-c->strokeMotion[1].x),double(raw.y-c->strokeMotion[1].y))>=16))
c->strokeMotion.erase(c->strokeMotion.begin());
auto chooseAxis=[](POINT from,POINT to) {
LONG dx=to.x-from.x,dy=to.y-from.y;
constexpr double diagonalBoundary=2.414213562373095; // tan(67.5 degrees)
if(std::abs(dx)>std::abs(dy)*diagonalBoundary) return 1;
if(std::abs(dy)>std::abs(dx)*diagonalBoundary) return 2;
return (dx<0)==(dy<0)?3:4;
};
auto distance=[](int axis,POINT from,POINT to) {
double dx=to.x-from.x,dy=to.y-from.y;
return axis==1?std::abs(dy):axis==2?std::abs(dx):
std::abs(axis==3?dy-dx:dy+dx)/std::sqrt(2.0);
};
if(!c->strokeAxisSettled && (c->end.x!=anchor.x || c->end.y!=anchor.y)) {
c->strokeAxis=chooseAxis(anchor,c->end);
// A one-pixel first movement must not lock a diagonal gesture to an axis.
c->strokeAxisSettled=std::hypot(double(raw.x-anchor.x),double(raw.y-anchor.y))>=8;
}
else if(c->strokeAxis && distance(c->strokeAxis,anchor,c->end)>50) {
// Keep the completed segment and turn at its endpoint.
auto corner=c->stroke.points.back();
int nextAxis=chooseAxis(c->strokeMotion.front(),raw);
if(nextAxis!=c->strokeAxis) {
c->strokeAnchor=c->stroke.points.size()-1;
anchor=corner;
c->strokeAxis=nextAxis;
}
}
if(c->strokeAxis==1) c->end.y=anchor.y;
else if(c->strokeAxis==2) c->end.x=anchor.x;
else if(c->strokeAxis>=3) {
int sign=c->strokeAxis==3?1:-1;
LONG delta=std::lround(((c->end.x-anchor.x)+sign*(c->end.y-anchor.y))/2.0);
LONG minY=sign==1?-anchor.y:anchor.y-(c->image->height-1);
LONG maxY=sign==1?c->image->height-1-anchor.y:anchor.y;
delta=std::clamp(delta,std::max(-anchor.x,minY),std::min(LONG(c->image->width-1)-anchor.x,maxY));
c->end={anchor.x+delta,anchor.y+sign*delta};
}
// Only the current segment is a preview; earlier corners stay fixed.
c->stroke.points.resize(c->strokeAnchor+1);
}
c->strokeConstrained=constrained;
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;
+12 -9
View File
@@ -1375,6 +1375,14 @@ struct SnippetView
std::vector<Snippet> snippets;
int selection = -1;
};
void updateSnippetEditor(HWND w, SnippetView *v)
{
bool selected = v->selection >= 0 && v->selection < (int)v->snippets.size();
value(w, 302, selected ? v->snippets[v->selection].trigger : L"");
value(w, 303, selected ? v->snippets[v->selection].text : L"");
for (int id : {302, 303, 311, 312, 313})
EnableWindow(GetDlgItem(w, id), selected);
}
void fillSnippets(HWND w, SnippetView *v)
{
SendDlgItemMessageW(w, 301, LB_RESETCONTENT, 0, 0);
@@ -1382,6 +1390,7 @@ void fillSnippets(HWND w, SnippetView *v)
SendDlgItemMessageW(w, 301, LB_ADDSTRING, 0, (LPARAM)sn.trigger.c_str());
if (v->selection >= 0)
SendDlgItemMessageW(w, 301, LB_SETCURSEL, v->selection, 0);
updateSnippetEditor(w, v);
}
void collectSnippet(HWND w, SnippetView *v)
{
@@ -1418,11 +1427,7 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
collectSnippet(w, v);
v->selection = (int)SendDlgItemMessageW(w, 301, LB_GETCURSEL, 0, 0);
if (v->selection >= 0)
{
value(w, 302, v->snippets[v->selection].trigger);
value(w, 303, v->snippets[v->selection].text);
}
updateSnippetEditor(w, v);
return 0;
}
if (id == 310)
@@ -1431,8 +1436,6 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
v->snippets.push_back({L";new", L""});
v->selection = (int)v->snippets.size() - 1;
fillSnippets(w, v);
value(w, 302, L";new");
value(w, 303, L"");
SetFocus(GetDlgItem(w, 302));
return 0;
}
@@ -1443,13 +1446,13 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
v->snippets.erase(v->snippets.begin() + v->selection);
v->selection = -1;
fillSnippets(w, v);
value(w, 302, L"");
value(w, 303, L"");
SetFocus(GetDlgItem(w, 301));
}
return 0;
}
if (id == 312 || id == 313)
{
if (v->selection < 0 || v->selection >= (int)v->snippets.size()) return 0;
SendDlgItemMessageW(w, 303, EM_REPLACESEL, TRUE, (LPARAM)(id == 312 ? L"{cursor}" : L"{enter}"));
return 0;
}
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.16";
inline constexpr wchar_t AppVersion[] = L"0.6.17";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;