Release v0.6.54: adjustable pencil smoothing and unsaved settings state
This commit is contained in:
1 parent
727dc96908
commit
d7520cdd7b
14 files changed
+318
-18
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.53 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.54 LANGUAGES CXX RC)
|
||||||
if(NOT WIN32)
|
if(NOT WIN32)
|
||||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ Double-click the tray icon to open Settings, the bold default menu item.
|
|||||||
Closing Settings with unsaved changes (including accepted auto-correction or OCR options)
|
Closing Settings with unsaved changes (including accepted auto-correction or OCR options)
|
||||||
asks whether to save, discard changes, or keep editing. Unchanged settings close without a prompt.
|
asks whether to save, discard changes, or keep editing. Unchanged settings close without a prompt.
|
||||||
If saving fails, the window stays open with your edits intact.
|
If saving fails, the window stays open with your edits intact.
|
||||||
|
Settings displays **No unsaved changes** with **Save** disabled until an edit is made.
|
||||||
|
**Unsaved changes — click Save to apply** appears for pending edits, including accepted
|
||||||
|
chat-language, auto-correction and OCR changes. Cancelling a child dialog or restoring
|
||||||
|
the original values clears its pending changes. Snippets saved in their own window
|
||||||
|
remain saved independently; the Settings indicator reflects any other pending edits.
|
||||||
Menus support access keys: press the underlined letter while the menu is open (for example,
|
Menus support access keys: press the underlined letter while the menu is open (for example,
|
||||||
S for Settings or X for Exit in the tray menu). In forms, press Alt+the underlined letter
|
S for Settings or X for Exit in the tray menu). In forms, press Alt+the underlined letter
|
||||||
to activate a button, toggle a checkbox or focus a labeled field. Dialog underline visibility
|
to activate a button, toggle a checkbox or focus a labeled field. Dialog underline visibility
|
||||||
@@ -206,6 +211,18 @@ after rotation. Vertex edits support undo/redo, and the rotation and thickness h
|
|||||||
to resize it and the round handle to rotate it (Shift snaps to 15 degrees). Arcs support
|
to resize it and the round handle to rotate it (Shift snaps to 15 degrees). Arcs support
|
||||||
color, thickness, line style, moving, cloning, undo/redo and PNG export.
|
color, thickness, line style, moving, cloning, undo/redo and PNG export.
|
||||||
|
|
||||||
|
**Smoothing (0–100%)** on the screenshot toolbar adjusts pencil paths. With a pencil
|
||||||
|
stroke selected it affects that stroke; otherwise it sets the default for new strokes
|
||||||
|
(initially 25%). **Apply to all strokes** applies the displayed amount to all pencil
|
||||||
|
strokes in the current image and sets the default for new ones. Shapes, text and
|
||||||
|
highlights are unchanged. Zero restores the original path; higher values reduce more
|
||||||
|
jitter. The original samples are retained through moving, endpoint editing and cloning,
|
||||||
|
so lowering the amount does not accumulate distortion. Each slider drag or bulk change
|
||||||
|
is one undo step, and PNG export uses the displayed geometry. Very short strokes remain
|
||||||
|
unchanged. Shift-constrained and linked strokes start without automatic smoothing;
|
||||||
|
you can explicitly adjust them afterwards. Shape/straight-line recognition is separate
|
||||||
|
and does not turn recognized shapes back into pencil strokes.
|
||||||
|
|
||||||
**Trapezoid (B)** on the toolbar creates a trapezoid with parallel bases. Pencil recognition also
|
**Trapezoid (B)** on the toolbar creates a trapezoid with parallel bases. Pencil recognition also
|
||||||
fits nearly parallel, unequal bases into a clean trapezoid. Trapezoids support bounding-box resizing,
|
fits nearly parallel, unequal bases into a clean trapezoid. Trapezoids support bounding-box resizing,
|
||||||
rotation, styling and undo/redo; Ctrl+Z after recognition restores the original pencil stroke.
|
rotation, styling and undo/redo; Ctrl+Z after recognition restores the original pencil stroke.
|
||||||
|
|||||||
Binary file not shown.
+7
-7
@@ -1,16 +1,16 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.53 — 2026-11-06
|
## v0.6.54 — 2026-11-06
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Include messenger chat language profiles in settings synchronization: Telegram Desktop, Telegram Web K in Chrome, Teams and saved inactive Viber profiles. Manual export/import continues to include them. Viber recognition remains disabled.
|
- Add **Smoothing (0–100%)** to the screenshot editor toolbar. Adjust the selected pencil stroke, or set the default for new strokes when no pencil stroke is selected (initially 25%). **Apply to all strokes** updates all pencil strokes in the image and the default. Text, highlights and geometric shapes are unchanged.
|
||||||
- Merge profiles by application and chat name. Independent chat changes merge; conflicting language lists and deletion versus editing of the same chat use the existing per-setting conflict choices. Preserve each profile's language order and propagate deletions.
|
- Retain original pencil samples: zero restores the original path, and reducing smoothing restores detail without accumulating distortion. Moving, endpoint editing, cloning, undo/redo and PNG export preserve the chosen geometry. Each slider drag or bulk change is one undo step. Very short strokes remain unchanged; Shift-constrained and linked strokes start unsmoothed but can be adjusted explicitly. Shape recognition remains separate.
|
||||||
- Preserve saved profile IDs when a layout is missing on a PC; switching skips unavailable layouts and falls back to the global cycle if none remain. **Keep this PC's layouts** keeps the global layout cycle and shortcuts local while chat profiles still synchronize.
|
- Show **No unsaved changes** in Settings with **Save** disabled until edits are pending. Show **Unsaved changes — click Save to apply** for pending edits, including accepted chat-language, OCR and automatic-correction preferences. Cancelling a child dialog or restoring the original values updates the state immediately. Separately saved snippets do not falsely mark Settings as unsaved.
|
||||||
- Preserve local chat profiles when downloading older shared settings that do not contain the chat field. An explicit empty chat map from the new version removes profiles. Update every synchronizing PC before sharing the new format.
|
- Avoid creating empty personal-dictionary entries when accepting unchanged automatic-correction options, preventing a false unsaved-state indication.
|
||||||
- Retain the **Arc (U)** screenshot tool from v0.6.52.
|
- Retain chat-language synchronization from v0.6.53 and **Arc (U)** from v0.6.52.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
Passed 126 sync checks (18 new checks for chat profiles), 674 chat-profile checks, 568 input checks, 332 native checks, dictionary equivalence (140,939 words), wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress checks. Chat sync checks cover upload/download, older-profile migration, independent merges, persisted conflict resolution, deletion, Unicode names, layout order, missing layouts, partial synchronization and malformed data rejection. Published assets, versions, checksums, update selection and bilingual Wiki pages are verified during publication.
|
Passed 774 settings/chat-profile checks, 568 input checks, 332 native checks, 126 sync checks, dictionary equivalence (140,939 words), wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress checks. Smoothing checks cover defaults, per-stroke and bulk edits, reversible geometry, movement/cloning/endpoints, undo grouping and exact PNG restoration at zero. Settings checks cover clean/dirty/reverted/invalid states and accepted/cancelled child changes. Inspected actual editor and Settings renderings, including Settings at 250% scaling. Published assets, checksums, executable versions, updater selection and bilingual Wiki pages are verified during publication.
|
||||||
|
|
||||||
## Upgrade and compatibility
|
## Upgrade and compatibility
|
||||||
Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
18ed8074c5c9a064b667612721389967f6fb758d7162abbcf4b73437724d2606 KSwitcher-win-x64.zip
|
e7a0d76f89d7d15123378390e1b549000404c4be2463160de95f9c1626687a59 KSwitcher-win-x64.zip
|
||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,6,53,0
|
FILEVERSION 0,6,54,0
|
||||||
PRODUCTVERSION 0,6,53,0
|
PRODUCTVERSION 0,6,54,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.6.53"
|
VALUE "FileVersion", "0.6.54"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+76
-4
@@ -178,6 +178,8 @@ struct Capture
|
|||||||
Mark textDefaults{};
|
Mark textDefaults{};
|
||||||
ScreenshotShape shape=ScreenshotShape::None;
|
ScreenshotShape shape=ScreenshotShape::None;
|
||||||
LineStyle lineStyle=LineStyle::Solid;
|
LineStyle lineStyle=LineStyle::Solid;
|
||||||
|
int smoothing=25;
|
||||||
|
bool smoothingDrag=false;
|
||||||
LineEnds arrowEnds=LineEnds::End, freehandEnds=LineEnds::None;
|
LineEnds arrowEnds=LineEnds::End, freehandEnds=LineEnds::None;
|
||||||
COLORREF currentColor=RGB(225,42,50);
|
COLORREF currentColor=RGB(225,42,50);
|
||||||
COLORREF customColors[16]{};
|
COLORREF customColors[16]{};
|
||||||
@@ -630,6 +632,13 @@ bool selectedLine(const Capture &c)
|
|||||||
}
|
}
|
||||||
void syncLineOptions(HWND w,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,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());
|
EnableWindow(GetDlgItem(w,35),c.selected>=0 && c.marks[c.selected].isLine());
|
||||||
bool selected=selectedLine(c),enabled=selected || c.arrow || c.freehand;
|
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);
|
SendDlgItemMessageW(w,28,WM_SETFONT,(WPARAM)c.font,TRUE);
|
||||||
MoveWindow(GetDlgItem(w,28),MulDiv(648,dpi,96),MulDiv(6,dpi,96),
|
MoveWindow(GetDlgItem(w,28),MulDiv(648,dpi,96),MulDiv(6,dpi,96),
|
||||||
MulDiv(78,dpi,96),MulDiv(32,dpi,96),TRUE);
|
MulDiv(78,dpi,96),MulDiv(32,dpi,96),TRUE);
|
||||||
c.panelHeight=c.unit+MulDiv(40,dpi,96);
|
c.panelHeight=c.unit+MulDiv(80,dpi,96);
|
||||||
for(int id:{30,31,32,33,34,35}) SendDlgItemMessageW(w,id,WM_SETFONT,(WPARAM)c.font,TRUE);
|
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,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,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,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);
|
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");
|
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);
|
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();
|
c.syncHistory();
|
||||||
auto work = monitor.rcWork;
|
auto work = monitor.rcWork;
|
||||||
SetWindowPos(w,HWND_NOTOPMOST,work.left+20,work.top+20,
|
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:
|
case WM_SETCURSOR:
|
||||||
if ((HWND)wp==w && LOWORD(lp)==HTCLIENT) { captureCursor(w,*c); return TRUE; }
|
if ((HWND)wp==w && LOWORD(lp)==HTCLIENT) { captureCursor(w,*c); return TRUE; }
|
||||||
break;
|
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:
|
case WM_COMMAND:
|
||||||
if(LOWORD(wp)==40) {
|
if(LOWORD(wp)==40) {
|
||||||
if(c->inlineText && HIWORD(wp)==EN_CHANGE) { readInlineText(*c); positionInlineText(*c); }
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
if (c->dragging) 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) {
|
if(LOWORD(wp)==35) {
|
||||||
finishInlineText(w,*c,true);
|
finishInlineText(w,*c,true);
|
||||||
if(c->selected>=0 && c->marks[c->selected].isLine()) beginMarkText(w,*c,c->selected);
|
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));
|
c->remember(c->marks); c->marks.push_back(std::move(c->stroke));
|
||||||
if(c->freehand) {
|
if(c->freehand) {
|
||||||
auto &mark=c->marks.back();
|
auto &mark=c->marks.back();
|
||||||
|
mark.rawPoints=mark.points;
|
||||||
linkScreenshotEndpoint(mark,true,c->start,c->marks,6);
|
linkScreenshotEndpoint(mark,true,c->start,c->marks,6);
|
||||||
linkScreenshotEndpoint(mark,false,c->end,c->marks,6);
|
linkScreenshotEndpoint(mark,false,c->end,c->marks,6);
|
||||||
resolveScreenshotLinks(c->marks);
|
resolveScreenshotLinks(c->marks);
|
||||||
@@ -1553,10 +1601,18 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
ScreenshotShape recognized{};
|
ScreenshotShape recognized{};
|
||||||
bool straightLine=false;
|
bool straightLine=false;
|
||||||
auto improved=improveScreenshotStroke(c->marks.back().points,&recognized,&straightLine);
|
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()) {
|
if(!improved.empty()) {
|
||||||
c->remember(c->marks);
|
c->remember(c->marks);
|
||||||
auto &mark=c->marks.back();
|
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();
|
mark.from=mark.points.front(); mark.to=mark.points.back();
|
||||||
if(straightLine) { mark.points.clear(); mark.freehand=false; }
|
if(straightLine) { mark.points.clear(); mark.freehand=false; }
|
||||||
if(recognized!=ScreenshotShape::None) {
|
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;
|
POINT start=from?from->from:m.from, end=to?to->from:m.to;
|
||||||
if(from) m.from=rim(*from,end,true);
|
if(from) m.from=rim(*from,end,true);
|
||||||
if(to) m.to=rim(*to,start,false);
|
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;
|
state[index]=2;
|
||||||
};
|
};
|
||||||
@@ -2068,10 +2131,15 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
|
|||||||
return low<=high?std::clamp(delta,low,high):0L;
|
return low<=high?std::clamp(delta,low,high):0L;
|
||||||
};
|
};
|
||||||
auto bounds=markBounds(m);
|
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 dx=shift(position.x-origin.x,bounds.left,bounds.right,width);
|
||||||
LONG dy=shift(position.y-origin.y,bounds.top,bounds.bottom,height);
|
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;
|
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.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)
|
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.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) {
|
if(m.shape!=ScreenshotShape::None && m.rotation!=0) {
|
||||||
auto fixedBefore=rotateShapePoint(original,part==MarkPart::Start?original.to:original.from);
|
auto fixedBefore=rotateShapePoint(original,part==MarkPart::Start?original.to:original.from);
|
||||||
auto fixedAfter=rotateShapePoint(m,part==MarkPart::Start?m.to:m.from);
|
auto fixedAfter=rotateShapePoint(m,part==MarkPart::Start?m.to:m.from);
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ struct ScreenshotMark {
|
|||||||
bool labelRotationManual=false;
|
bool labelRotationManual=false;
|
||||||
unsigned long long id{};
|
unsigned long long id{};
|
||||||
ScreenshotLink fromLink, toLink;
|
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 isText() const { return !text.empty(); }
|
||||||
bool isHighlight() const { return !points.empty() && !freehand && shape==ScreenshotShape::None; }
|
bool isHighlight() const { return !points.empty() && !freehand && shape==ScreenshotShape::None; }
|
||||||
bool isLine() const { return !number && !isText() && !isHighlight() && 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 {
|
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.
|
// Returns a replacement path; an empty result means no visible improvement.
|
||||||
// Report recognized geometry separately from a smoothed freehand path.
|
// Report recognized geometry separately from a smoothed freehand path.
|
||||||
inline std::vector<POINT> improveScreenshotStroke(const std::vector<POINT> &input, ScreenshotShape *shape=nullptr,
|
inline std::vector<POINT> improveScreenshotStroke(const std::vector<POINT> &input, ScreenshotShape *shape=nullptr,
|
||||||
|
|||||||
+40
-1
@@ -968,7 +968,9 @@ struct SettingsView
|
|||||||
int selection = -1;
|
int selection = -1;
|
||||||
bool filling{};
|
bool filling{};
|
||||||
std::vector<std::vector<HWND>> pages;
|
std::vector<std::vector<HWND>> pages;
|
||||||
|
bool ready{}, refreshing{};
|
||||||
};
|
};
|
||||||
|
void updateSettingsState(HWND w, SettingsView *v);
|
||||||
void selectSettingsPage(HWND w, SettingsView *v)
|
void selectSettingsPage(HWND w, SettingsView *v)
|
||||||
{
|
{
|
||||||
int selected = TabCtrl_GetCurSel(GetDlgItem(w, 260));
|
int selected = TabCtrl_GetCurSel(GetDlgItem(w, 260));
|
||||||
@@ -1169,6 +1171,7 @@ void fillSettings(HWND w, SettingsView *v)
|
|||||||
v->selection = 0;
|
v->selection = 0;
|
||||||
fillLayouts(w, v);
|
fillLayouts(w, v);
|
||||||
v->filling = false;
|
v->filling = false;
|
||||||
|
updateSettingsState(w, v);
|
||||||
}
|
}
|
||||||
int integer(HWND w, int id)
|
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);
|
auto words = parseCommaList(text);
|
||||||
std::sort(words.begin(), words.end());
|
std::sort(words.begin(), words.end());
|
||||||
words.erase(std::unique(words.begin(), words.end()), 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.autoExcluded = parseCommaList(value(w, 404));
|
||||||
draft.autoPrefixChars = value(w, 408);
|
draft.autoPrefixChars = value(w, 408);
|
||||||
@@ -1535,6 +1541,27 @@ bool settingsChanged(HWND w, SettingsView *v)
|
|||||||
return true;
|
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)
|
void closeSettings(HWND w, SettingsView *v)
|
||||||
{
|
{
|
||||||
if (settingsChanged(w, v))
|
if (settingsChanged(w, v))
|
||||||
@@ -1583,6 +1610,12 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
{
|
{
|
||||||
switch (msg)
|
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:
|
case WM_NOTIFY:
|
||||||
if (((NMHDR *)lp)->idFrom == 260 && ((NMHDR *)lp)->code == TCN_SELCHANGE)
|
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;
|
break;
|
||||||
case WM_COMMAND:
|
case WM_COMMAND:
|
||||||
{
|
{
|
||||||
|
SettingsStateRefresh refresh{w};
|
||||||
int id = LOWORD(wp);
|
int id = LOWORD(wp);
|
||||||
if (id == 220 && HIWORD(wp) == LBN_SELCHANGE && !v->filling)
|
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 (id == IDOK)
|
||||||
{
|
{
|
||||||
|
if (!settingsChanged(w, v)) return 0;
|
||||||
if (v->app->busy)
|
if (v->app->busy)
|
||||||
throw std::runtime_error("Wait for the current text operation to finish");
|
throw std::runtime_error("Wait for the current text operation to finish");
|
||||||
v->app->applySettings(collectSettings(w, v));
|
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);
|
auto *parent = (SettingsView *)GetWindowLongPtrW(v->app->settingsWindow, GWLP_USERDATA);
|
||||||
parent->draft.snippets = s.snippets;
|
parent->draft.snippets = s.snippets;
|
||||||
|
updateSettingsState(v->app->settingsWindow, parent);
|
||||||
}
|
}
|
||||||
DestroyWindow(w);
|
DestroyWindow(w);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -2069,11 +2105,14 @@ void showSettings(App &app, bool visible)
|
|||||||
checkbox(w, L"Write lo&g", 208, 24, 378, 210);
|
checkbox(w, L"Write lo&g", 208, 24, 378, 210);
|
||||||
page();
|
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"Impo&rt...", 250, 18, 466, 125);
|
||||||
button(w, L"&Export...", 251, 155, 466, 125);
|
button(w, L"&Export...", 251, 155, 466, 125);
|
||||||
button(w, L"&Save", IDOK, 490, 466, 100);
|
button(w, L"&Save", IDOK, 490, 466, 100);
|
||||||
button(w, L"&Cancel", IDCANCEL, 602, 466, 100);
|
button(w, L"&Cancel", IDCANCEL, 602, 466, 100);
|
||||||
fillSettings(w, v);
|
fillSettings(w, v);
|
||||||
|
v->ready = true;
|
||||||
|
updateSettingsState(w, v);
|
||||||
TabCtrl_SetCurSel(tabs, 0);
|
TabCtrl_SetCurSel(tabs, 0);
|
||||||
selectSettingsPage(w, v);
|
selectSettingsPage(w, v);
|
||||||
if (visible)
|
if (visible)
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
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 UpdateMessage = WM_APP + 7;
|
||||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||||
|
|||||||
@@ -121,6 +121,52 @@ int main()
|
|||||||
app.settings.chatCycles[key] = {app.layouts[0].id};
|
app.settings.chatCycles[key] = {app.layouts[0].id};
|
||||||
ks::showSettings(app);
|
ks::showSettings(app);
|
||||||
HWND parent = app.settingsWindow;
|
HWND parent = app.settingsWindow;
|
||||||
|
auto saveEnabled=[&] { return IsWindowEnabled(GetDlgItem(parent,IDOK))!=FALSE; };
|
||||||
|
auto status=[&] { wchar_t text[256]{}; GetDlgItemTextW(parent,261,text,256); return std::wstring(text); };
|
||||||
|
require(!saveEnabled() && status()==L"No unsaved changes.","new Settings shows clean state with Save disabled");
|
||||||
|
SendMessageW(parent,WM_COMMAND,IDOK,0);
|
||||||
|
require(IsWindow(parent),"Save command with no changes does not apply or close Settings");
|
||||||
|
bool originalLog=IsDlgButtonChecked(parent,208)==BST_CHECKED;
|
||||||
|
CheckDlgButton(parent,208,originalLog?BST_UNCHECKED:BST_CHECKED);
|
||||||
|
SendMessageW(parent,WM_COMMAND,MAKEWPARAM(208,BN_CLICKED),0);
|
||||||
|
require(saveEnabled() && status().find(L"Unsaved changes") == 0,"checkbox edit immediately marks Settings dirty");
|
||||||
|
CheckDlgButton(parent,208,originalLog?BST_CHECKED:BST_UNCHECKED);
|
||||||
|
SendMessageW(parent,WM_COMMAND,MAKEWPARAM(208,BN_CLICKED),0);
|
||||||
|
require(!saveEnabled(),"reverting checkbox disables Save again");
|
||||||
|
wchar_t originalLimit[32]{}; GetDlgItemTextW(parent,240,originalLimit,32);
|
||||||
|
SetDlgItemTextW(parent,240,L"invalid");
|
||||||
|
require(saveEnabled(),"invalid text remains an unsaved edit");
|
||||||
|
SetDlgItemTextW(parent,240,originalLimit);
|
||||||
|
require(!saveEnabled(),"restoring valid original text clears dirty state");
|
||||||
|
SendMessageW(parent,WM_COMMAND,257,0);
|
||||||
|
HWND ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
|
||||||
|
require(ocr!=nullptr,"OCR preferences open");
|
||||||
|
wchar_t languages[128]{}; GetDlgItemTextW(ocr,503,languages,128);
|
||||||
|
SetDlgItemTextW(ocr,503,L"eng");
|
||||||
|
SendMessageW(ocr,WM_COMMAND,IDCANCEL,0);
|
||||||
|
require(!saveEnabled(),"cancelled child changes leave Settings clean");
|
||||||
|
SendMessageW(parent,WM_COMMAND,257,0);
|
||||||
|
ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
|
||||||
|
SetDlgItemTextW(ocr,503,std::wstring(languages)==L"eng"?L"ukr":L"eng");
|
||||||
|
SendMessageW(ocr,WM_COMMAND,IDOK,0);
|
||||||
|
require(!IsWindow(ocr) && saveEnabled(),"accepted OCR edits enable parent Save");
|
||||||
|
SendMessageW(parent,WM_COMMAND,257,0);
|
||||||
|
ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
|
||||||
|
SetDlgItemTextW(ocr,503,languages);
|
||||||
|
SendMessageW(ocr,WM_COMMAND,IDOK,0);
|
||||||
|
require(!saveEnabled(),"reverting OCR draft in child clears parent dirty state");
|
||||||
|
SendMessageW(parent,WM_COMMAND,255,0);
|
||||||
|
HWND automatic=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
|
||||||
|
require(automatic!=nullptr,"automatic correction preferences open");
|
||||||
|
bool replace=IsDlgButtonChecked(automatic,414)==BST_CHECKED;
|
||||||
|
CheckDlgButton(automatic,414,replace?BST_UNCHECKED:BST_CHECKED);
|
||||||
|
SendMessageW(automatic,WM_COMMAND,IDOK,0);
|
||||||
|
require(!IsWindow(automatic) && saveEnabled(),"accepted auto-correction edits enable Save");
|
||||||
|
SendMessageW(parent,WM_COMMAND,255,0);
|
||||||
|
automatic=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
|
||||||
|
CheckDlgButton(automatic,414,replace?BST_CHECKED:BST_UNCHECKED);
|
||||||
|
SendMessageW(automatic,WM_COMMAND,IDOK,0);
|
||||||
|
require(!saveEnabled(),"reverting auto-correction draft disables Save");
|
||||||
HWND tabs = GetDlgItem(parent, 260);
|
HWND tabs = GetDlgItem(parent, 260);
|
||||||
require(tabs && TabCtrl_GetItemCount(tabs) == 5, "five settings categories");
|
require(tabs && TabCtrl_GetItemCount(tabs) == 5, "five settings categories");
|
||||||
for (int page = 0; page < 5; ++page)
|
for (int page = 0; page < 5; ++page)
|
||||||
@@ -151,6 +197,7 @@ int main()
|
|||||||
TabCtrl_SetCurSel(tabs, 1);
|
TabCtrl_SetCurSel(tabs, 1);
|
||||||
NMHDR notification{tabs, 260, TCN_SELCHANGE};
|
NMHDR notification{tabs, 260, TCN_SELCHANGE};
|
||||||
SendMessageW(parent, WM_NOTIFY, 260, (LPARAM)¬ification);
|
SendMessageW(parent, WM_NOTIFY, 260, (LPARAM)¬ification);
|
||||||
|
require(!saveEnabled(),"changing tabs leaves Settings clean");
|
||||||
SendMessageW(parent, WM_COMMAND, 259, 0);
|
SendMessageW(parent, WM_COMMAND, 259, 0);
|
||||||
HWND dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
HWND dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
||||||
require(dialog && !IsWindowEnabled(parent), "chat dialog opens with disabled parent");
|
require(dialog && !IsWindowEnabled(parent), "chat dialog opens with disabled parent");
|
||||||
@@ -169,6 +216,7 @@ int main()
|
|||||||
SendDlgItemMessageW(dialog, 711, LB_SETSEL, TRUE, -1);
|
SendDlgItemMessageW(dialog, 711, LB_SETSEL, TRUE, -1);
|
||||||
SendMessageW(dialog, WM_COMMAND, IDCANCEL, 0);
|
SendMessageW(dialog, WM_COMMAND, IDCANCEL, 0);
|
||||||
require(!IsWindow(dialog) && IsWindowEnabled(parent), "cancel closes dialog");
|
require(!IsWindow(dialog) && IsWindowEnabled(parent), "cancel closes dialog");
|
||||||
|
require(!saveEnabled(),"cancelled chat edits do not enable Save");
|
||||||
SendMessageW(parent, WM_COMMAND, 259, 0);
|
SendMessageW(parent, WM_COMMAND, 259, 0);
|
||||||
dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
||||||
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
||||||
@@ -178,20 +226,30 @@ int main()
|
|||||||
SendDlgItemMessageW(dialog, 711, LB_SETSEL, TRUE, -1);
|
SendDlgItemMessageW(dialog, 711, LB_SETSEL, TRUE, -1);
|
||||||
SendMessageW(dialog, WM_COMMAND, IDOK, 0);
|
SendMessageW(dialog, WM_COMMAND, IDOK, 0);
|
||||||
require(!IsWindow(dialog) && IsWindowEnabled(parent), "profile applies to settings draft");
|
require(!IsWindow(dialog) && IsWindowEnabled(parent), "profile applies to settings draft");
|
||||||
|
require(saveEnabled() == (app.layouts.size()>1),"accepted chat edits refresh Save immediately");
|
||||||
SendMessageW(parent, WM_COMMAND, IDOK, 0);
|
SendMessageW(parent, WM_COMMAND, IDOK, 0);
|
||||||
require(!app.settingsWindow && app.settings.chatCycles.at(key).size() == app.layouts.size(), "settings saves selected languages");
|
require(!app.settingsWindow && app.settings.chatCycles.at(key).size() == app.layouts.size(), "settings saves selected languages");
|
||||||
require(ks::loadSettings(root / L"settings.json", app.layouts).chatCycles == app.settings.chatCycles, "profile saved to disk");
|
require(ks::loadSettings(root / L"settings.json", app.layouts).chatCycles == app.settings.chatCycles, "profile saved to disk");
|
||||||
ks::showSettings(app);
|
ks::showSettings(app);
|
||||||
parent = app.settingsWindow;
|
parent = app.settingsWindow;
|
||||||
|
require(!saveEnabled(),"reopened saved Settings is clean");
|
||||||
SendMessageW(parent, WM_COMMAND, 259, 0);
|
SendMessageW(parent, WM_COMMAND, 259, 0);
|
||||||
dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
||||||
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
||||||
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
|
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
|
||||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
||||||
SendMessageW(dialog, WM_COMMAND, 713, 0);
|
SendMessageW(dialog, WM_COMMAND, 713, 0);
|
||||||
|
require(saveEnabled() && status().find(L"Unsaved changes")==0,"removing child profile immediately marks parent dirty");
|
||||||
|
RedrawWindow(parent,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
|
||||||
|
GetClientRect(parent,&rect); dc=GetDC(parent);
|
||||||
|
clip=ks::screenshotClip(dc,rect.right,rect.bottom,{}); ReleaseDC(parent,dc);
|
||||||
|
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path()/L"settings-unsaved.png",
|
||||||
|
ks::encodePng(ks::decodeImage(clip)));
|
||||||
SendMessageW(parent, WM_COMMAND, IDOK, 0);
|
SendMessageW(parent, WM_COMMAND, IDOK, 0);
|
||||||
require(!app.settings.chatCycles.contains(key), "remove profile restores default");
|
require(!app.settings.chatCycles.contains(key), "remove profile restores default");
|
||||||
app.settings.snippets = {{L";scope", L"Example", L"scope-fixture"}};
|
app.settings.snippets = {{L";scope", L"Example", L"scope-fixture"}};
|
||||||
|
ks::showSettings(app); parent=app.settingsWindow;
|
||||||
|
require(!saveEnabled(),"Settings starts clean before separately saved snippet edit");
|
||||||
ks::showSnippets(app);
|
ks::showSnippets(app);
|
||||||
HWND snippets = app.snippetsWindow;
|
HWND snippets = app.snippetsWindow;
|
||||||
SendDlgItemMessageW(snippets,301,LB_SETCURSEL,0,0);
|
SendDlgItemMessageW(snippets,301,LB_SETCURSEL,0,0);
|
||||||
@@ -202,6 +260,8 @@ int main()
|
|||||||
SetDlgItemTextW(snippets,315,L"Notepad.exe, MS-Teams.EXE");
|
SetDlgItemTextW(snippets,315,L"Notepad.exe, MS-Teams.EXE");
|
||||||
SendMessageW(snippets,WM_COMMAND,IDOK,0);
|
SendMessageW(snippets,WM_COMMAND,IDOK,0);
|
||||||
require(!app.snippetsWindow && app.settings.snippets[0].apps==std::vector<std::wstring>{L"notepad",L"ms-teams"}, "snippet editor saves selected applications");
|
require(!app.snippetsWindow && app.settings.snippets[0].apps==std::vector<std::wstring>{L"notepad",L"ms-teams"}, "snippet editor saves selected applications");
|
||||||
|
require(!saveEnabled() && status()==L"No unsaved changes.","separately saved snippet edit leaves Settings clean");
|
||||||
|
SendMessageW(parent,WM_COMMAND,IDCANCEL,0);
|
||||||
ks::showSnippets(app);snippets=app.snippetsWindow;
|
ks::showSnippets(app);snippets=app.snippetsWindow;
|
||||||
SendDlgItemMessageW(snippets,301,LB_SETCURSEL,0,0);
|
SendDlgItemMessageW(snippets,301,LB_SETCURSEL,0,0);
|
||||||
SendMessageW(snippets,WM_COMMAND,MAKEWPARAM(301,LBN_SELCHANGE),0);
|
SendMessageW(snippets,WM_COMMAND,MAKEWPARAM(301,LBN_SELCHANGE),0);
|
||||||
|
|||||||
@@ -83,6 +83,53 @@ int main(int argc, char **argv)
|
|||||||
require(c.pencilCursor!=nullptr,"pencil drawing cursor loads");
|
require(c.pencilCursor!=nullptr,"pencil drawing cursor loads");
|
||||||
require(c.freehand && !c.pointer && ks::toolCursor(c)==c.pencilCursor,
|
require(c.freehand && !c.pointer && ks::toolCursor(c)==c.pencilCursor,
|
||||||
"new screenshot editor starts with the pencil tool");
|
"new screenshot editor starts with the pencil tool");
|
||||||
|
{
|
||||||
|
require(GetDlgItem(window,41) && GetDlgItem(window,44),"smoothing slider and Apply to all exist");
|
||||||
|
auto adjust=[&](int value,int code=TB_THUMBPOSITION) {
|
||||||
|
SendDlgItemMessageW(window,41,TBM_SETPOS,TRUE,value);
|
||||||
|
SendMessageW(window,WM_HSCROLL,MAKEWPARAM(code,0),(LPARAM)GetDlgItem(window,41));
|
||||||
|
};
|
||||||
|
adjust(70); require(c.smoothing==70 && c.marks.empty(),"no selection changes default smoothing");
|
||||||
|
ks::Mark pencil{{40,80},{240,80},0}; pencil.freehand=true;
|
||||||
|
for(int i=0;i<=100;++i) pencil.points.push_back({40+i*2,80+(i%2?5:0)});
|
||||||
|
ks::Mark rectangle{{40,130},{240,210},0}; rectangle.shape=ks::ScreenshotShape::Rectangle;
|
||||||
|
c.marks={pencil,pencil,rectangle}; c.selected=0;
|
||||||
|
auto rawImage=ks::decodeImage(ks::screenshotClip(c.image->dc,320,240,c.marks)).bgra;
|
||||||
|
adjust(40,TB_THUMBTRACK); adjust(90,TB_THUMBTRACK); adjust(90,TB_ENDTRACK);
|
||||||
|
require(c.marks[0].smoothing==90 && c.marks[1].smoothing==0 && c.smoothing==70,
|
||||||
|
"selected stroke smoothing leaves other objects and default alone");
|
||||||
|
SendMessageW(window,WM_COMMAND,12,0);
|
||||||
|
require(c.marks[0].smoothing==0,"one undo reverses an entire slider drag");
|
||||||
|
SendMessageW(window,WM_COMMAND,18,0);
|
||||||
|
require(c.marks[0].smoothing==90,"redo restores smoothing amount and path");
|
||||||
|
c.selected=0; ks::syncLineOptions(window,c);
|
||||||
|
SendMessageW(window,WM_COMMAND,44,0);
|
||||||
|
require(c.marks[0].smoothing==90 && c.marks[1].smoothing==90 && c.smoothing==90 &&
|
||||||
|
c.marks[2].shape==ks::ScreenshotShape::Rectangle && c.marks[2].rawPoints.empty(),
|
||||||
|
"Apply to all updates pencil strokes and default without changing shapes");
|
||||||
|
require(ks::decodeImage(ks::screenshotClip(c.image->dc,320,240,c.marks)).bgra!=rawImage,
|
||||||
|
"PNG export uses the smoothed geometry");
|
||||||
|
SendMessageW(window,WM_COMMAND,12,0);
|
||||||
|
require(c.marks[0].smoothing==90 && c.marks[1].smoothing==0,"bulk smoothing is one undo step");
|
||||||
|
c.selected=0; adjust(0);
|
||||||
|
require(c.marks[0].points.size()==pencil.points.size() && c.marks[0].points[1].y==85,
|
||||||
|
"selected zero smoothing restores exact original path");
|
||||||
|
require(ks::decodeImage(ks::screenshotClip(c.image->dc,320,240,c.marks)).bgra==rawImage,
|
||||||
|
"restoring zero smoothing restores the exact original exported pixels");
|
||||||
|
c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{};
|
||||||
|
adjust(75);
|
||||||
|
auto mouse=[&](UINT message,POINT p) {
|
||||||
|
int x=c.view.left+MulDiv(p.x,c.view.right-c.view.left,c.image->width);
|
||||||
|
int y=c.view.top+MulDiv(p.y,c.view.bottom-c.view.top,c.image->height);
|
||||||
|
SendMessageW(window,message,message==WM_LBUTTONUP?0:MK_LBUTTON,MAKELPARAM(x,y));
|
||||||
|
};
|
||||||
|
mouse(WM_LBUTTONDOWN,{40,90});
|
||||||
|
for(int i=1;i<=100;++i) mouse(WM_MOUSEMOVE,{40+i*2,90+LONG(std::lround(35*std::sin(i*.04)+3*std::sin(i*1.7)))});
|
||||||
|
mouse(WM_LBUTTONUP,{240,64});
|
||||||
|
require(c.marks.size()==1 && c.marks[0].freehand && c.marks[0].smoothing==75 && !c.marks[0].rawPoints.empty(),
|
||||||
|
"new curved pencil stroke uses toolbar default and retains original samples");
|
||||||
|
c.marks.clear(); c.selected=-1; c.history=ks::ScreenshotHistory{}; c.smoothing=25;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
require(GetDlgItem(window,36)!=nullptr,"triangle has a dedicated toolbar button");
|
require(GetDlgItem(window,36)!=nullptr,"triangle has a dedicated toolbar button");
|
||||||
SendMessageW(window,WM_KEYDOWN,'G',0);
|
SendMessageW(window,WM_KEYDOWN,'G',0);
|
||||||
|
|||||||
@@ -122,4 +122,31 @@ inline void testStrokeImprovement() {
|
|||||||
double deviation=0; for(auto p:smooth) deviation+=std::abs(p.y-100);
|
double deviation=0; for(auto p:smooth) deviation+=std::abs(p.y-100);
|
||||||
require(deviation/smooth.size()<1.5 && smooth.front().y==jitter.front().y && smooth.back().y==jitter.back().y,
|
require(deviation/smooth.size()<1.5 && smooth.front().y==jitter.front().y && smooth.back().y==jitter.back().y,
|
||||||
"smoothing reduces jitter and preserves endpoints");
|
"smoothing reduces jitter and preserves endpoints");
|
||||||
|
auto equal=[](const auto &a,const auto &b){return a.size()==b.size() && std::equal(a.begin(),a.end(),b.begin(),
|
||||||
|
[](POINT x,POINT y){return x.x==y.x && x.y==y.y;});};
|
||||||
|
require(equal(ks::smoothScreenshotStroke(jitter,0),jitter),"zero smoothing preserves exact mouse samples");
|
||||||
|
std::vector<POINT> waves;
|
||||||
|
for(int i=0;i<=200;++i) waves.push_back({30+i,100+LONG(std::lround(6*std::sin(i*.5)))});
|
||||||
|
auto roughness=[](const auto &p){double total=0; for(size_t i=1;i<p.size();++i) total+=std::abs(p[i].y-p[i-1].y); return total;};
|
||||||
|
auto low=ks::smoothScreenshotStroke(waves,25),high=ks::smoothScreenshotStroke(waves,100);
|
||||||
|
require(roughness(high)<roughness(low) && roughness(low)<roughness(waves),"higher smoothing removes more jitter");
|
||||||
|
require(high.front().x==waves.front().x && high.front().y==waves.front().y &&
|
||||||
|
high.back().x==waves.back().x && high.back().y==waves.back().y,"smoothing preserves both endpoints");
|
||||||
|
ks::ScreenshotMark mark{waves.front(),waves.back(),0}; mark.freehand=true; mark.points=waves;
|
||||||
|
ks::setScreenshotSmoothing(mark,100); ks::setScreenshotSmoothing(mark,25);
|
||||||
|
require(equal(mark.points,low),"lowering smoothing recomputes from original instead of accumulating filters");
|
||||||
|
ks::setScreenshotSmoothing(mark,0);
|
||||||
|
require(equal(mark.points,waves),"zero restores original after multiple adjustments");
|
||||||
|
ks::setScreenshotSmoothing(mark,100);
|
||||||
|
auto moved=ks::dragScreenshotMark(mark,ks::MarkPart::Body,{0,0},{10,15},500,500);
|
||||||
|
ks::setScreenshotSmoothing(moved,0);
|
||||||
|
require(moved.points[40].x==waves[40].x+10 && moved.points[40].y==waves[40].y+15,
|
||||||
|
"moving preserves the original path in moved coordinates");
|
||||||
|
auto resized=ks::dragScreenshotMark(mark,ks::MarkPart::End,mark.to,{260,130},500,500);
|
||||||
|
ks::setScreenshotSmoothing(resized,0);
|
||||||
|
require(resized.points.back().x==260 && resized.points.back().y==130,"endpoint editing survives smoothing changes");
|
||||||
|
auto duplicate=ks::cloneScreenshotMark(mark,{mark},500,500);
|
||||||
|
ks::setScreenshotSmoothing(duplicate,0);
|
||||||
|
require(duplicate.points[40].x==waves[40].x+16 && duplicate.points[40].y==waves[40].y+16,
|
||||||
|
"cloning retains independent original geometry");
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user