Release v0.6.54: adjustable pencil smoothing and unsaved settings state
This commit is contained in:
1 parent
727dc96908
commit
63b7b879a3
14 files changed
+318
-18
No files matched your search
+76
-4
@@ -178,6 +178,8 @@ struct Capture
|
||||
Mark textDefaults{};
|
||||
ScreenshotShape shape=ScreenshotShape::None;
|
||||
LineStyle lineStyle=LineStyle::Solid;
|
||||
int smoothing=25;
|
||||
bool smoothingDrag=false;
|
||||
LineEnds arrowEnds=LineEnds::End, freehandEnds=LineEnds::None;
|
||||
COLORREF currentColor=RGB(225,42,50);
|
||||
COLORREF customColors[16]{};
|
||||
@@ -630,6 +632,13 @@ bool selectedLine(const Capture &c)
|
||||
}
|
||||
void syncLineOptions(HWND w,const Capture &c)
|
||||
{
|
||||
bool pencil=c.selected>=0 && c.marks[c.selected].freehand;
|
||||
int amount=pencil?c.marks[c.selected].smoothing:c.smoothing;
|
||||
SendDlgItemMessageW(w,41,TBM_SETPOS,TRUE,amount);
|
||||
auto smoothingText=L"Smoothing: "+std::to_wstring(amount)+L"%";
|
||||
SetDlgItemTextW(w,42,smoothingText.c_str());
|
||||
SetDlgItemTextW(w,43,pencil?L"Selected stroke":L"New pencil strokes");
|
||||
EnableWindow(GetDlgItem(w,44),std::any_of(c.marks.begin(),c.marks.end(),[](const Mark &m){return m.freehand;}));
|
||||
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;
|
||||
@@ -689,8 +698,12 @@ void layout(HWND w, Capture &c)
|
||||
SendDlgItemMessageW(w,28,WM_SETFONT,(WPARAM)c.font,TRUE);
|
||||
MoveWindow(GetDlgItem(w,28),MulDiv(648,dpi,96),MulDiv(6,dpi,96),
|
||||
MulDiv(78,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);
|
||||
c.panelHeight=c.unit+MulDiv(80,dpi,96);
|
||||
for(int id:{30,31,32,33,34,35,41,42,43,44}) SendDlgItemMessageW(w,id,WM_SETFONT,(WPARAM)c.font,TRUE);
|
||||
MoveWindow(GetDlgItem(w,42),MulDiv(12,dpi,96),c.unit+MulDiv(44,dpi,96),MulDiv(128,dpi,96),MulDiv(24,dpi,96),TRUE);
|
||||
MoveWindow(GetDlgItem(w,41),MulDiv(142,dpi,96),c.unit+MulDiv(40,dpi,96),MulDiv(200,dpi,96),MulDiv(30,dpi,96),TRUE);
|
||||
MoveWindow(GetDlgItem(w,43),MulDiv(350,dpi,96),c.unit+MulDiv(44,dpi,96),MulDiv(150,dpi,96),MulDiv(24,dpi,96),TRUE);
|
||||
MoveWindow(GetDlgItem(w,44),MulDiv(516,dpi,96),c.unit+MulDiv(40,dpi,96),MulDiv(210,dpi,96),MulDiv(30,dpi,96),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);
|
||||
@@ -1072,6 +1085,14 @@ void edit(HWND w, Capture &c)
|
||||
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);
|
||||
INITCOMMONCONTROLSEX sliders{sizeof(sliders),ICC_BAR_CLASSES}; InitCommonControlsEx(&sliders);
|
||||
CreateWindowW(TRACKBAR_CLASSW,L"Pencil smoothing",WS_CHILD|WS_VISIBLE|WS_TABSTOP|TBS_NOTICKS,
|
||||
0,0,0,0,w,(HMENU)41,c.app.instance,nullptr);
|
||||
SendDlgItemMessageW(w,41,TBM_SETRANGE,TRUE,MAKELPARAM(0,100));
|
||||
SendDlgItemMessageW(w,41,TBM_SETPAGESIZE,0,10);
|
||||
for(int id:{42,43}) CreateWindowW(L"STATIC",L"",WS_CHILD|WS_VISIBLE,0,0,0,0,w,(HMENU)(INT_PTR)id,c.app.instance,nullptr);
|
||||
CreateWindowW(L"BUTTON",L"Apply to all strokes",WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON,
|
||||
0,0,0,0,w,(HMENU)44,c.app.instance,nullptr);
|
||||
c.syncHistory();
|
||||
auto work = monitor.rcWork;
|
||||
SetWindowPos(w,HWND_NOTOPMOST,work.left+20,work.top+20,
|
||||
@@ -1140,6 +1161,21 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case WM_SETCURSOR:
|
||||
if ((HWND)wp==w && LOWORD(lp)==HTCLIENT) { captureCursor(w,*c); return TRUE; }
|
||||
break;
|
||||
case WM_HSCROLL:
|
||||
if((HWND)lp==GetDlgItem(w,41) && !c->dragging) {
|
||||
int amount=int(SendDlgItemMessageW(w,41,TBM_GETPOS,0,0));
|
||||
bool pencil=c->selected>=0 && c->marks[c->selected].freehand;
|
||||
if(pencil && c->marks[c->selected].smoothing!=amount) {
|
||||
if(!c->smoothingDrag) c->remember(c->marks);
|
||||
setScreenshotSmoothing(c->marks[c->selected],amount);
|
||||
resolveScreenshotLinks(c->marks);
|
||||
c->smoothingDrag=LOWORD(wp)==TB_THUMBTRACK;
|
||||
} else if(!pencil) c->smoothing=amount;
|
||||
if(LOWORD(wp)!=TB_THUMBTRACK) c->smoothingDrag=false;
|
||||
syncLineOptions(w,*c); InvalidateRect(w,nullptr,FALSE);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_COMMAND:
|
||||
if(LOWORD(wp)==40) {
|
||||
if(c->inlineText && HIWORD(wp)==EN_CHANGE) { readInlineText(*c); positionInlineText(*c); }
|
||||
@@ -1147,6 +1183,17 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
return 0;
|
||||
}
|
||||
if (c->dragging) return 0;
|
||||
if(LOWORD(wp)==44) {
|
||||
int amount=int(SendDlgItemMessageW(w,41,TBM_GETPOS,0,0));
|
||||
c->smoothing=amount;
|
||||
if(std::any_of(c->marks.begin(),c->marks.end(),[&](const Mark &m){return m.freehand && m.smoothing!=amount;})) {
|
||||
c->remember(c->marks);
|
||||
for(auto &mark:c->marks) if(mark.freehand) setScreenshotSmoothing(mark,amount);
|
||||
resolveScreenshotLinks(c->marks);
|
||||
}
|
||||
c->smoothingDrag=false; syncLineOptions(w,*c); InvalidateRect(w,nullptr,FALSE);
|
||||
return 0;
|
||||
}
|
||||
if(LOWORD(wp)==35) {
|
||||
finishInlineText(w,*c,true);
|
||||
if(c->selected>=0 && c->marks[c->selected].isLine()) beginMarkText(w,*c,c->selected);
|
||||
@@ -1543,6 +1590,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
c->remember(c->marks); c->marks.push_back(std::move(c->stroke));
|
||||
if(c->freehand) {
|
||||
auto &mark=c->marks.back();
|
||||
mark.rawPoints=mark.points;
|
||||
linkScreenshotEndpoint(mark,true,c->start,c->marks,6);
|
||||
linkScreenshotEndpoint(mark,false,c->end,c->marks,6);
|
||||
resolveScreenshotLinks(c->marks);
|
||||
@@ -1553,10 +1601,18 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
ScreenshotShape recognized{};
|
||||
bool straightLine=false;
|
||||
auto improved=improveScreenshotStroke(c->marks.back().points,&recognized,&straightLine);
|
||||
if(recognized==ScreenshotShape::None && !straightLine) {
|
||||
improved=smoothScreenshotStroke(c->marks.back().rawPoints,c->smoothing);
|
||||
const auto &raw=c->marks.back().rawPoints;
|
||||
if(improved.size()==raw.size() && std::equal(improved.begin(),improved.end(),raw.begin(),
|
||||
[](POINT a,POINT b){return a.x==b.x && a.y==b.y;})) improved.clear();
|
||||
}
|
||||
if(!improved.empty()) {
|
||||
c->remember(c->marks);
|
||||
auto &mark=c->marks.back();
|
||||
mark.points=std::move(improved);
|
||||
if(recognized==ScreenshotShape::None && !straightLine) {
|
||||
setScreenshotSmoothing(mark,c->smoothing);
|
||||
} else { mark.points=std::move(improved); mark.rawPoints.clear(); }
|
||||
mark.from=mark.points.front(); mark.to=mark.points.back();
|
||||
if(straightLine) { mark.points.clear(); mark.freehand=false; }
|
||||
if(recognized!=ScreenshotShape::None) {
|
||||
@@ -1914,7 +1970,14 @@ void resolveScreenshotLinks(std::vector<ScreenshotMark> &marks)
|
||||
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; }
|
||||
if(m.freehand && !m.points.empty()) {
|
||||
m.points.front()=m.from; m.points.back()=m.to;
|
||||
if(!m.rawPoints.empty() && (m.rawPoints.front().x!=m.from.x || m.rawPoints.front().y!=m.from.y ||
|
||||
m.rawPoints.back().x!=m.to.x || m.rawPoints.back().y!=m.to.y)) {
|
||||
m.rawPoints.front()=m.from; m.rawPoints.back()=m.to;
|
||||
setScreenshotSmoothing(m,m.smoothing);
|
||||
}
|
||||
}
|
||||
}
|
||||
state[index]=2;
|
||||
};
|
||||
@@ -2068,10 +2131,15 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
|
||||
return low<=high?std::clamp(delta,low,high):0L;
|
||||
};
|
||||
auto bounds=markBounds(m);
|
||||
for(auto p:m.rawPoints) {
|
||||
bounds.left=std::min(bounds.left,p.x); bounds.right=std::max(bounds.right,p.x);
|
||||
bounds.top=std::min(bounds.top,p.y); bounds.bottom=std::max(bounds.bottom,p.y);
|
||||
}
|
||||
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; }
|
||||
for(auto &point:m.rawPoints) { point.x+=dx; point.y+=dy; }
|
||||
}
|
||||
else if(part==MarkPart::Start || part==MarkPart::End)
|
||||
{
|
||||
@@ -2096,6 +2164,10 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
|
||||
}
|
||||
}
|
||||
if(m.freehand && !m.points.empty()) (part==MarkPart::Start?m.points.front():m.points.back())=endpoint;
|
||||
if(m.freehand && !m.rawPoints.empty()) {
|
||||
(part==MarkPart::Start?m.rawPoints.front():m.rawPoints.back())=endpoint;
|
||||
setScreenshotSmoothing(m,m.smoothing);
|
||||
}
|
||||
if(m.shape!=ScreenshotShape::None && m.rotation!=0) {
|
||||
auto fixedBefore=rotateShapePoint(original,part==MarkPart::Start?original.to:original.from);
|
||||
auto fixedAfter=rotateShapePoint(m,part==MarkPart::Start?m.to:m.from);
|
||||
|
||||
@@ -39,6 +39,8 @@ struct ScreenshotMark {
|
||||
bool labelRotationManual=false;
|
||||
unsigned long long id{};
|
||||
ScreenshotLink fromLink, toLink;
|
||||
std::vector<POINT> rawPoints; // Original pencil path for reversible smoothing.
|
||||
int smoothing=0; // 0..100; applies only to freehand strokes.
|
||||
bool isText() const { return !text.empty(); }
|
||||
bool isHighlight() const { return !points.empty() && !freehand && shape==ScreenshotShape::None; }
|
||||
bool isLine() const { return !number && !isText() && !isHighlight() && shape==ScreenshotShape::None; }
|
||||
|
||||
@@ -38,6 +38,42 @@ inline double cross(Point a, Point b, Point c) { return (b.x-a.x)*(c.y-a.y)-(b.y
|
||||
}
|
||||
|
||||
namespace ks {
|
||||
// Filter by distance along the path, not mouse-event count. Always start from
|
||||
// the original path so lowering the amount restores detail without drift.
|
||||
inline std::vector<POINT> smoothScreenshotStroke(const std::vector<POINT> &input, int amount) {
|
||||
amount=std::clamp(amount,0,100);
|
||||
if(!amount || input.size()<5) return input;
|
||||
using namespace stroke_detail;
|
||||
std::vector<Point> raw;
|
||||
for(auto p:input) if(raw.empty() || distance(raw.back(),{double(p.x),double(p.y)})>0)
|
||||
raw.push_back({double(p.x),double(p.y)});
|
||||
double total=length(raw);
|
||||
if(raw.size()<5 || total<16) return input;
|
||||
auto points=sample(raw,size_t(std::clamp(std::ceil(total/2)+1,3.0,4096.0)));
|
||||
double step=total/(points.size()-1),radius=0.5+32*std::pow(amount/100.0,2);
|
||||
int reach=std::max(1,int(std::ceil(radius/step)));
|
||||
std::vector<POINT> result;
|
||||
for(size_t i=0;i<points.size();++i) {
|
||||
Point sum{}; double weights=0;
|
||||
for(int j=std::max(0,int(i)-reach);j<=std::min(int(points.size())-1,int(i)+reach);++j) {
|
||||
double d=(j-double(i))*step,weight=std::exp(-2*d*d/(radius*radius));
|
||||
sum.x+=points[j].x*weight; sum.y+=points[j].y*weight; weights+=weight;
|
||||
}
|
||||
double blend=std::min(1.0,std::min(i,points.size()-1-i)*step/radius);
|
||||
POINT p{LONG(std::lround(points[i].x+blend*(sum.x/weights-points[i].x))),
|
||||
LONG(std::lround(points[i].y+blend*(sum.y/weights-points[i].y)))};
|
||||
if(result.empty() || p.x!=result.back().x || p.y!=result.back().y) result.push_back(p);
|
||||
}
|
||||
result.front()=input.front(); result.back()=input.back();
|
||||
return result;
|
||||
}
|
||||
inline void setScreenshotSmoothing(ScreenshotMark &mark,int amount) {
|
||||
if(!mark.freehand || mark.shape!=ScreenshotShape::None || mark.points.empty()) return;
|
||||
if(mark.rawPoints.empty()) mark.rawPoints=mark.points;
|
||||
mark.smoothing=std::clamp(amount,0,100);
|
||||
mark.points=smoothScreenshotStroke(mark.rawPoints,mark.smoothing);
|
||||
mark.from=mark.points.front(); mark.to=mark.points.back();
|
||||
}
|
||||
// Returns a replacement path; an empty result means no visible improvement.
|
||||
// Report recognized geometry separately from a smoothed freehand path.
|
||||
inline std::vector<POINT> improveScreenshotStroke(const std::vector<POINT> &input, ScreenshotShape *shape=nullptr,
|
||||
|
||||
+40
-1
@@ -968,7 +968,9 @@ struct SettingsView
|
||||
int selection = -1;
|
||||
bool filling{};
|
||||
std::vector<std::vector<HWND>> pages;
|
||||
bool ready{}, refreshing{};
|
||||
};
|
||||
void updateSettingsState(HWND w, SettingsView *v);
|
||||
void selectSettingsPage(HWND w, SettingsView *v)
|
||||
{
|
||||
int selected = TabCtrl_GetCurSel(GetDlgItem(w, 260));
|
||||
@@ -1169,6 +1171,7 @@ void fillSettings(HWND w, SettingsView *v)
|
||||
v->selection = 0;
|
||||
fillLayouts(w, v);
|
||||
v->filling = false;
|
||||
updateSettingsState(w, v);
|
||||
}
|
||||
int integer(HWND w, int id)
|
||||
{
|
||||
@@ -1298,7 +1301,10 @@ LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
auto words = parseCommaList(text);
|
||||
std::sort(words.begin(), words.end());
|
||||
words.erase(std::unique(words.begin(), words.end()), words.end());
|
||||
draft.autoWords[language] = std::move(words);
|
||||
// Opening and accepting an untouched empty dictionary must not
|
||||
// introduce a new entry and mark Settings as changed.
|
||||
if (!words.empty() || draft.autoWords.contains(language))
|
||||
draft.autoWords[language] = std::move(words);
|
||||
}
|
||||
draft.autoExcluded = parseCommaList(value(w, 404));
|
||||
draft.autoPrefixChars = value(w, 408);
|
||||
@@ -1535,6 +1541,27 @@ bool settingsChanged(HWND w, SettingsView *v)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
void updateSettingsState(HWND w, SettingsView *v)
|
||||
{
|
||||
if (!v || !v->ready || v->filling || v->refreshing) return;
|
||||
v->refreshing = true;
|
||||
bool changed = settingsChanged(w, v);
|
||||
EnableWindow(GetDlgItem(w, IDOK), changed);
|
||||
const wchar_t *text = changed ? L"Unsaved changes — click Save to apply." : L"No unsaved changes.";
|
||||
if (value(w, 261) != text) value(w, 261, text);
|
||||
v->refreshing = false;
|
||||
}
|
||||
// Refresh after commands have updated controls/drafts, including early returns.
|
||||
// Saving can destroy Settings, so retrieve its state only if it still exists.
|
||||
struct SettingsStateRefresh
|
||||
{
|
||||
HWND window;
|
||||
~SettingsStateRefresh()
|
||||
{
|
||||
if (IsWindow(window))
|
||||
updateSettingsState(window, reinterpret_cast<SettingsView *>(GetWindowLongPtrW(window, GWLP_USERDATA)));
|
||||
}
|
||||
};
|
||||
void closeSettings(HWND w, SettingsView *v)
|
||||
{
|
||||
if (settingsChanged(w, v))
|
||||
@@ -1583,6 +1610,12 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case WM_ENABLE:
|
||||
if (wp) updateSettingsState(w, v);
|
||||
break;
|
||||
case WM_ACTIVATE:
|
||||
if (LOWORD(wp) != WA_INACTIVE) updateSettingsState(w, v);
|
||||
break;
|
||||
case WM_NOTIFY:
|
||||
if (((NMHDR *)lp)->idFrom == 260 && ((NMHDR *)lp)->code == TCN_SELCHANGE)
|
||||
{
|
||||
@@ -1592,6 +1625,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
break;
|
||||
case WM_COMMAND:
|
||||
{
|
||||
SettingsStateRefresh refresh{w};
|
||||
int id = LOWORD(wp);
|
||||
if (id == 220 && HIWORD(wp) == LBN_SELCHANGE && !v->filling)
|
||||
{
|
||||
@@ -1627,6 +1661,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
if (id == IDOK)
|
||||
{
|
||||
if (!settingsChanged(w, v)) return 0;
|
||||
if (v->app->busy)
|
||||
throw std::runtime_error("Wait for the current text operation to finish");
|
||||
v->app->applySettings(collectSettings(w, v));
|
||||
@@ -1846,6 +1881,7 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
auto *parent = (SettingsView *)GetWindowLongPtrW(v->app->settingsWindow, GWLP_USERDATA);
|
||||
parent->draft.snippets = s.snippets;
|
||||
updateSettingsState(v->app->settingsWindow, parent);
|
||||
}
|
||||
DestroyWindow(w);
|
||||
return 0;
|
||||
@@ -2069,11 +2105,14 @@ void showSettings(App &app, bool visible)
|
||||
checkbox(w, L"Write lo&g", 208, 24, 378, 210);
|
||||
page();
|
||||
|
||||
control(w, L"STATIC", L"No unsaved changes.", 0, 261, 18, 430, 684, 24);
|
||||
button(w, L"Impo&rt...", 250, 18, 466, 125);
|
||||
button(w, L"&Export...", 251, 155, 466, 125);
|
||||
button(w, L"&Save", IDOK, 490, 466, 100);
|
||||
button(w, L"&Cancel", IDCANCEL, 602, 466, 100);
|
||||
fillSettings(w, v);
|
||||
v->ready = true;
|
||||
updateSettingsState(w, v);
|
||||
TabCtrl_SetCurSel(tabs, 0);
|
||||
selectSettingsPage(w, v);
|
||||
if (visible)
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.53";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.54";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
Reference in new issue
Block a user