Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbf20e8270 | |||
| b8bb14a985 | |||
| d28e28c03c |
@@ -75,9 +75,11 @@ physical key positions and therefore reset recent-word tracking; selection conve
|
|||||||
Press **Win+Shift+S**, choose **Capture screen region…** in the tray menu, or run
|
Press **Win+Shift+S**, choose **Capture screen region…** in the tray menu, or run
|
||||||
`KSwitcher.exe --capture`. Drag a rectangle on the dimmed, frozen desktop; the selected area stays
|
`KSwitcher.exe --capture`. Drag a rectangle on the dimmed, frozen desktop; the selected area stays
|
||||||
at normal brightness. Escape cancels. Selection and annotation frames are buffered to avoid flicker.
|
at normal brightness. Escape cancels. Selection and annotation frames are buffered to avoid flicker.
|
||||||
The selected area is copied immediately and opens in the editor, even when Windows notifications
|
The selected area is copied immediately, the fullscreen selection is hidden, and a Windows notification
|
||||||
are disabled, suppressed or cannot be sent. Disable **Open editor after screen capture** in settings
|
is sent. Click the notification to open the editor. Enable **Open editor after screen capture** in settings
|
||||||
to open it only by clicking the notification; an immediate notification error still opens the editor.
|
to open it automatically; this option is off by default. An immediate notification error still opens the editor.
|
||||||
|
Existing saved preferences are preserved. Windows Do Not Disturb can send notifications directly to the
|
||||||
|
notification center without showing a banner.
|
||||||
The preference applies to each new capture, including while other editors are open.
|
The preference applies to each new capture, including while other editors are open.
|
||||||
KSwitcher also sends a notification with a thumbnail. Notification identity and activation are registered
|
KSwitcher also sends a notification with a thumbnail. Notification identity and activation are registered
|
||||||
at resident startup and refreshed before sending; registration/submission errors are written to the log.
|
at resident startup and refreshed before sending; registration/submission errors are written to the log.
|
||||||
@@ -99,6 +101,13 @@ change its diameter. Wheel up increases the size; wheel down decreases it. These
|
|||||||
and update linked arrows. **Ctrl+D** clones the selected object with a small offset. A cloned number
|
and update linked arrows. **Ctrl+D** clones the selected object with a small offset. A cloned number
|
||||||
gets the smallest available positive number, just like a newly placed number: gaps left by deletion
|
gets the smallest available positive number, just like a newly placed number: gaps left by deletion
|
||||||
are filled first, without renumbering existing objects. A cloned arrow keeps its style but starts without endpoint links.
|
are filled first, without renumbering existing objects. A cloned arrow keeps its style but starts without endpoint links.
|
||||||
|
Arrow and line endpoints can attach to numbered markers, rectangles, circles, ellipses, text blocks,
|
||||||
|
and other arrows or strokes. Drop an endpoint onto the target; use **Ctrl+drag** to start a new
|
||||||
|
connector over an existing object. Attached endpoints follow movement and resizing; text attachments
|
||||||
|
also follow rotation. Arrow-to-arrow attachments retain their relative position along the target line.
|
||||||
|
Clicking a connector or adding/editing its caption preserves its links. Moving the whole connector
|
||||||
|
intentionally detaches its endpoints; moving one endpoint detaches only that end. Deleting a target
|
||||||
|
leaves connected endpoints at their last position. Self-links and circular dependency chains are rejected.
|
||||||
The compact toolbar uses [Lucide](https://lucide.dev) icons with hover tooltips showing each action
|
The compact toolbar uses [Lucide](https://lucide.dev) icons with hover tooltips showing each action
|
||||||
and its keyboard shortcut.
|
and its keyboard shortcut.
|
||||||
Drop either arrow endpoint onto a number to link it to the circle's edge. Linked endpoints follow
|
Drop either arrow endpoint onto a number to link it to the circle's edge. Linked endpoints follow
|
||||||
@@ -158,7 +167,9 @@ Captures use ordinary clipboard history settings and limits. Protected video and
|
|||||||
|
|
||||||
Select **Update** in the tray menu to check the stable Releases of
|
Select **Update** in the tray menu to check the stable Releases of
|
||||||
[this repository](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/releases).
|
[this repository](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/releases).
|
||||||
A startup check also reports an available version, but never installs it automatically.
|
A background check runs shortly after startup and every 6 hours while KSwitcher is running.
|
||||||
|
It reports each available version once per session and never installs automatically.
|
||||||
|
Network failures stay in diagnostics; the next scheduled check retries. You can also check manually at any time.
|
||||||
Confirm the offered version to download and install it. The updater uses the existing
|
Confirm the offered version to download and install it. The updater uses the existing
|
||||||
`KSwitcher-win-x64.zip` and `SHA256SUMS.txt` assets, checks the checksum, package contents, x64
|
`KSwitcher-win-x64.zip` and `SHA256SUMS.txt` assets, checks the checksum, package contents, x64
|
||||||
executable identity and version, then closes and restarts KSwitcher. Downloads and staging happen
|
executable identity and version, then closes and restarts KSwitcher. Downloads and staging happen
|
||||||
|
|||||||
Binary file not shown.
+11
-10
@@ -1,17 +1,18 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.2 — 2026-10-29
|
## v0.6.5 — 2026-10-29
|
||||||
|
|
||||||
- Add **Open editor after screen capture**, enabled by default. Disable it to use notification-first capture; an immediate notification error still opens the editor.
|
## Changes
|
||||||
- Save the preference and apply it to each capture, including requests sent to an already running screenshot process.
|
- Preserve endpoint links when selecting a connector or adding/editing its caption through double-click, F2 or the caption button. A click without movement no longer detaches the connector.
|
||||||
- Register screenshot notification identity, shortcut and COM activation during resident startup, as well as before sending a notification.
|
- Attach arrow and line endpoints to rectangles, circles, ellipses, text blocks, other arrows/lines, and freehand/highlight strokes, in addition to numbered markers.
|
||||||
- Log notification registration and submission errors when logging is enabled.
|
- Keep attachments aligned as targets move or resize; text attachments also follow rotation. Arrow-to-arrow chains resolve in dependency order.
|
||||||
- Refresh clipboard history preview row heights when display DPI changes.
|
- Preserve links through Undo/Redo. Deleted targets leave endpoints at their last position; clones start without links. Self-links and circular dependencies are rejected.
|
||||||
|
|
||||||
Verified: 175 native checks, 397 input/layout checks at 240 DPI, screenshot editor UI integration,
|
## Usage
|
||||||
isolated notification registration checks, and 45 updater checks. A physical transition between
|
Drop a connector endpoint onto the target. Use **Ctrl+drag** to start a new connector over an existing object. Moving a whole connector detaches both of its endpoints; dragging one endpoint detaches only that end.
|
||||||
monitors with different DPI has not been tested. Successful notification submission does not
|
|
||||||
guarantee a visible Windows banner; asynchronous toast failures are not yet monitored.
|
## Validation
|
||||||
|
176 native checks, 440 input/layout checks at 240 DPI, screenshot editor UI integration, isolated notification registration, and 45 updater checks. Coverage includes caption editing, shape-arrow-arrow chains, movement, resize, text rotation, deletion, cloning, cycle rejection, and Undo/Redo.
|
||||||
|
|
||||||
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the
|
**Upgrade:** 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.
|
v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
e0c5a7b4f225ee32790f01ec916ae4706783b7e50b0bf6816c38ae2a6685d43d KSwitcher-win-x64.zip
|
bea45fdb33657fdfd5b065cdf10a3c358febdd5e895672dd106ec5ef2dd16c9a 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,2,0
|
FILEVERSION 0,6,5,0
|
||||||
PRODUCTVERSION 0,6,2,0
|
PRODUCTVERSION 0,6,5,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.2"
|
VALUE "FileVersion", "0.6.5"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+6
-3
@@ -1254,8 +1254,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if (MessageBoxW(hwnd, prompt.c_str(), L"KSwitcher Update", MB_YESNO | MB_ICONQUESTION) == IDYES)
|
if (MessageBoxW(hwnd, prompt.c_str(), L"KSwitcher Update", MB_YESNO | MB_ICONQUESTION) == IDYES)
|
||||||
app->checkUpdate(true, true, result->version);
|
app->checkUpdate(true, true, result->version);
|
||||||
}
|
}
|
||||||
else
|
else if (app->notifiedUpdate != result->version)
|
||||||
{
|
{
|
||||||
|
app->notifiedUpdate = result->version;
|
||||||
NOTIFYICONDATAW icon{sizeof(icon)};
|
NOTIFYICONDATAW icon{sizeof(icon)};
|
||||||
icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; icon.dwInfoFlags = NIIF_INFO;
|
icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; icon.dwInfoFlags = NIIF_INFO;
|
||||||
wcscpy_s(icon.szInfoTitle, L"KSwitcher update available");
|
wcscpy_s(icon.szInfoTitle, L"KSwitcher update available");
|
||||||
@@ -1274,7 +1275,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
else if (result->interactive)
|
else if (result->interactive)
|
||||||
MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR);
|
MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR);
|
||||||
else
|
else
|
||||||
diagnosticEvent(L"Startup update check failed: " + result->error);
|
diagnosticEvent(L"Background update check failed: " + result->error);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_HOTKEY:
|
case WM_HOTKEY:
|
||||||
@@ -1338,7 +1339,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
case WM_TIMER:
|
case WM_TIMER:
|
||||||
if (wp == 5)
|
if (wp == 5)
|
||||||
{
|
{
|
||||||
KillTimer(hwnd, 5);
|
// After the initial 5-second check, repeat while the resident stays open.
|
||||||
|
// Rearm even when a check is already running or the previous one failed.
|
||||||
|
SetTimer(hwnd, 5, UpdateCheckIntervalMs, nullptr);
|
||||||
app->checkUpdate(false);
|
app->checkUpdate(false);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class App
|
|||||||
std::thread worker;
|
std::thread worker;
|
||||||
std::thread updateWorker;
|
std::thread updateWorker;
|
||||||
bool updateRunning{}, installingUpdate{};
|
bool updateRunning{}, installingUpdate{};
|
||||||
std::wstring availableUpdate;
|
std::wstring availableUpdate, notifiedUpdate;
|
||||||
bool stop{};
|
bool stop{};
|
||||||
UINT taskbarCreated{};
|
UINT taskbarCreated{};
|
||||||
uint64_t pendingAutoVersion{};
|
uint64_t pendingAutoVersion{};
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ struct Settings
|
|||||||
bool elevatedAutostart = false;
|
bool elevatedAutostart = false;
|
||||||
bool autoCorrect = false;
|
bool autoCorrect = false;
|
||||||
bool writeLog = true;
|
bool writeLog = true;
|
||||||
bool screenshotAutoOpenEditor = true;
|
bool screenshotAutoOpenEditor = false;
|
||||||
int autoMaxRank = 0;
|
int autoMaxRank = 0;
|
||||||
int autoMinLength = 3;
|
int autoMinLength = 3;
|
||||||
std::wstring autoPrefixChars = L"([{«“‘\"";
|
std::wstring autoPrefixChars = L"([{«“‘\"";
|
||||||
|
|||||||
+139
-24
@@ -3,6 +3,8 @@
|
|||||||
#include "../resources/resource.h"
|
#include "../resources/resource.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <functional>
|
||||||
|
#include <unordered_set>
|
||||||
#include <windowsx.h>
|
#include <windowsx.h>
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <commdlg.h>
|
#include <commdlg.h>
|
||||||
@@ -197,7 +199,7 @@ void updateStatus(Capture &c)
|
|||||||
else if(part==MarkPart::Rotation) text=L"Drag to rotate text | Hold Shift for 15-degree steps";
|
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::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::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(part==MarkPart::Start || part==MarkPart::End) text=L"Drag handle to resize | Endpoints can snap to objects and other arrows";
|
||||||
else if(c.selected>=0) {
|
else if(c.selected>=0) {
|
||||||
const auto &m=c.marks[c.selected];
|
const auto &m=c.marks[c.selected];
|
||||||
if(c.selectedLabel || part==MarkPart::Label)
|
if(c.selectedLabel || part==MarkPart::Label)
|
||||||
@@ -274,6 +276,7 @@ void finishInlineText(HWND w,Capture &c,bool commit)
|
|||||||
}
|
}
|
||||||
if(!empty) c.textDefaults=c.textDraft;
|
if(!empty) c.textDefaults=c.textDraft;
|
||||||
}
|
}
|
||||||
|
if(commit) resolveScreenshotLinks(c.marks);
|
||||||
c.textIndex=-1; c.editingLabel=false;
|
c.textIndex=-1; c.editingLabel=false;
|
||||||
InvalidateRect(w,nullptr,FALSE);
|
InvalidateRect(w,nullptr,FALSE);
|
||||||
}
|
}
|
||||||
@@ -575,6 +578,15 @@ 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};
|
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);
|
double tolerance=double(std::max(6,c.unit/6))*c.image->width/std::max(1L,c.view.right-c.view.left);
|
||||||
|
// Badges are painted above connectors. Their visible bodies must also win
|
||||||
|
// hit testing, even when an attached connector is currently selected.
|
||||||
|
for(int i=int(c.marks.size())-1;i>=0;--i) {
|
||||||
|
const auto &m=c.marks[i];
|
||||||
|
if(m.number && std::hypot(double(p.x-m.from.x),double(p.y-m.from.y))<=m.radius) {
|
||||||
|
auto part=hitScreenshotMark(m,p,tolerance);
|
||||||
|
return {i,part};
|
||||||
|
}
|
||||||
|
}
|
||||||
if(c.selected>=0)
|
if(c.selected>=0)
|
||||||
{
|
{
|
||||||
const auto &m=c.marks[c.selected];
|
const auto &m=c.marks[c.selected];
|
||||||
@@ -815,6 +827,9 @@ void finishSelection(HWND w,Capture &c,DWORD (*publish)(HWND,const Clip &)=write
|
|||||||
c.copied=true;
|
c.copied=true;
|
||||||
// A successful toast submission does not guarantee that Windows shows a banner.
|
// A successful toast submission does not guarantee that Windows shows a banner.
|
||||||
if(c.autoOpenEditor) edit(w,c);
|
if(c.autoOpenEditor) edit(w,c);
|
||||||
|
// Leave fullscreen capture before submitting the toast: Windows may suppress
|
||||||
|
// banners while a fullscreen application is visible.
|
||||||
|
else ShowWindow(w,SW_HIDE);
|
||||||
bool notified=c.app.notify(clip);
|
bool notified=c.app.notify(clip);
|
||||||
if(!c.autoOpenEditor) {
|
if(!c.autoOpenEditor) {
|
||||||
if(notified) DestroyWindow(w);
|
if(notified) DestroyWindow(w);
|
||||||
@@ -880,6 +895,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if(chooseTextFont(w,draft)) {
|
if(chooseTextFont(w,draft)) {
|
||||||
c->remember(c->marks); c->marks[c->selected]=draft;
|
c->remember(c->marks); c->marks[c->selected]=draft;
|
||||||
c->textDefaults=draft.isLine()?screenshotArrowLabel(draft):draft;
|
c->textDefaults=draft.isLine()?screenshotArrowLabel(draft):draft;
|
||||||
|
resolveScreenshotLinks(c->marks);
|
||||||
}
|
}
|
||||||
} else chooseTextFont(w,c->textDefaults);
|
} else chooseTextFont(w,c->textDefaults);
|
||||||
SetFocus(w); InvalidateRect(w,nullptr,FALSE); return 0;
|
SetFocus(w); InvalidateRect(w,nullptr,FALSE); return 0;
|
||||||
@@ -933,7 +949,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
auto &mark=c->marks[c->selected];
|
auto &mark=c->marks[c->selected];
|
||||||
int &value=mark.isText()?mark.fontSize:mark.thickness;
|
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);
|
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(next!=value) { c->remember(c->marks); value=next; resolveScreenshotLinks(c->marks); }
|
||||||
}
|
}
|
||||||
if (LOWORD(wp)==13) { copy(w,*c); return 0; }
|
if (LOWORD(wp)==13) { copy(w,*c); return 0; }
|
||||||
if (LOWORD(wp)==12 || LOWORD(wp)==18)
|
if (LOWORD(wp)==12 || LOWORD(wp)==18)
|
||||||
@@ -1123,7 +1139,8 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if (!c->editing) { finishSelection(w,*c); return 0; }
|
if (!c->editing) { finishSelection(w,*c); return 0; }
|
||||||
else if(c->part!=MarkPart::None)
|
else if(c->part!=MarkPart::None)
|
||||||
{
|
{
|
||||||
if(c->part==MarkPart::Start || c->part==MarkPart::End)
|
if((c->part==MarkPart::Start || c->part==MarkPart::End) &&
|
||||||
|
(c->start.x!=c->end.x || c->start.y!=c->end.y))
|
||||||
{
|
{
|
||||||
auto &mark=c->marks[c->selected];
|
auto &mark=c->marks[c->selected];
|
||||||
linkScreenshotEndpoint(mark,c->part==MarkPart::Start,c->end,c->marks,6);
|
linkScreenshotEndpoint(mark,c->part==MarkPart::Start,c->end,c->marks,6);
|
||||||
@@ -1350,27 +1367,107 @@ ScreenshotMark cloneScreenshotMark(const ScreenshotMark &original,const std::vec
|
|||||||
{
|
{
|
||||||
auto result=original;
|
auto result=original;
|
||||||
result.fromNumber=result.toNumber=0;
|
result.fromNumber=result.toNumber=0;
|
||||||
|
result.id=0; result.fromLink={}; result.toLink={};
|
||||||
if(result.number) result.number=nextScreenshotNumber(marks);
|
if(result.number) result.number=nextScreenshotNumber(marks);
|
||||||
auto bounds=markBounds(result);
|
auto bounds=markBounds(result);
|
||||||
LONG dx=bounds.right+16<width?16:-16;
|
LONG dx=bounds.right+16<width?16:-16;
|
||||||
LONG dy=bounds.bottom+16<height?16:-16;
|
LONG dy=bounds.bottom+16<height?16:-16;
|
||||||
return dragScreenshotMark(result,MarkPart::Body,{0,0},{dx,dy},width,height);
|
return dragScreenshotMark(result,MarkPart::Body,{0,0},{dx,dy},width,height);
|
||||||
}
|
}
|
||||||
void linkScreenshotEndpoint(ScreenshotMark &arrow, bool start, POINT drop,
|
namespace {
|
||||||
const std::vector<ScreenshotMark> &marks, double tolerance)
|
void assignMarkIds(std::vector<ScreenshotMark> &marks)
|
||||||
{
|
{
|
||||||
auto &link=start?arrow.fromNumber:arrow.toNumber;
|
static unsigned long long next=1;
|
||||||
link=0;
|
for(const auto &m:marks) next=std::max(next,m.id+1);
|
||||||
if(arrow.shape!=ScreenshotShape::None || arrow.isText()) return;
|
std::unordered_set<unsigned long long> used;
|
||||||
for(auto it=marks.rbegin();it!=marks.rend();++it)
|
for(auto &m:marks) {
|
||||||
if(it->number && std::hypot(double(drop.x-it->from.x),double(drop.y-it->from.y))<=it->radius+tolerance)
|
if(!m.id || !used.insert(m.id).second) { m.id=next++; used.insert(m.id); }
|
||||||
{
|
}
|
||||||
link=it->number;
|
}
|
||||||
break;
|
POINT objectAnchor(const ScreenshotMark &m,const ScreenshotLink &link)
|
||||||
|
{
|
||||||
|
if(m.isText() || m.shape!=ScreenshotShape::None) {
|
||||||
|
auto r=m.isText()?plainTextBounds(m):region(m.from,m.to);
|
||||||
|
POINT p{LONG(std::lround(r.left+(r.right-r.left)*link.u)),LONG(std::lround(r.top+(r.bottom-r.top)*link.v))};
|
||||||
|
return m.isText()?rotateTextPoint(m,p):p;
|
||||||
|
}
|
||||||
|
size_t segments=m.points.size()>1?m.points.size()-1:1;
|
||||||
|
double along=std::clamp(link.u,0.0,1.0)*segments;
|
||||||
|
size_t i=std::min(size_t(along),segments-1); double t=along-i;
|
||||||
|
POINT a=m.points.size()>1?m.points[i]:m.from,b=m.points.size()>1?m.points[i+1]:m.to;
|
||||||
|
return {LONG(std::lround(a.x+(b.x-a.x)*t)),LONG(std::lround(a.y+(b.y-a.y)*t))};
|
||||||
|
}
|
||||||
|
bool findObjectAnchor(const ScreenshotMark &m,POINT drop,double tolerance,ScreenshotLink &link)
|
||||||
|
{
|
||||||
|
link={m.id,0,0};
|
||||||
|
if(m.isText() || m.shape!=ScreenshotShape::None) {
|
||||||
|
auto r=m.isText()?plainTextBounds(m):region(m.from,m.to);
|
||||||
|
POINT p=m.isText()?rotateTextPoint(m,drop,-1):drop;
|
||||||
|
double w=std::max(1L,r.right-r.left),h=std::max(1L,r.bottom-r.top);
|
||||||
|
double x=(p.x-r.left)/w,y=(p.y-r.top)/h;
|
||||||
|
bool inside=x>=0 && x<=1 && y>=0 && y<=1;
|
||||||
|
if(m.shape==ScreenshotShape::Circle || m.shape==ScreenshotShape::Ellipse) {
|
||||||
|
double dx=2*x-1,dy=2*y-1,length=std::hypot(dx,dy);
|
||||||
|
inside=length<=1;
|
||||||
|
if(length<0.001) { dx=1; dy=0; length=1; }
|
||||||
|
link.u=(1+dx/length)/2; link.v=(1+dy/length)/2;
|
||||||
|
} else {
|
||||||
|
link.u=std::clamp(x,0.0,1.0); link.v=std::clamp(y,0.0,1.0);
|
||||||
|
if(inside) {
|
||||||
|
double edges[]{x*w,(1-x)*w,y*h,(1-y)*h};
|
||||||
|
auto side=std::min_element(edges,edges+4)-edges;
|
||||||
|
if(side<2) link.u=side==0?0:1; else link.v=side==2?0:1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
auto anchor=objectAnchor(m,link);
|
||||||
|
return inside || std::hypot(double(drop.x-anchor.x),double(drop.y-anchor.y))<=tolerance;
|
||||||
|
}
|
||||||
|
double best=1e100;
|
||||||
|
size_t segments=m.points.size()>1?m.points.size()-1:1;
|
||||||
|
for(size_t i=0;i<segments;++i) {
|
||||||
|
POINT a=m.points.size()>1?m.points[i]:m.from,b=m.points.size()>1?m.points[i+1]:m.to;
|
||||||
|
double dx=b.x-a.x,dy=b.y-a.y,length=dx*dx+dy*dy;
|
||||||
|
double t=length?std::clamp(((drop.x-a.x)*dx+(drop.y-a.y)*dy)/length,0.0,1.0):0;
|
||||||
|
double distance=std::hypot(drop.x-a.x-t*dx,drop.y-a.y-t*dy);
|
||||||
|
if(distance<best) { best=distance; link.u=(i+t)/segments; }
|
||||||
|
}
|
||||||
|
return best<=tolerance+m.thickness/2.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void linkScreenshotEndpoint(ScreenshotMark &arrow, bool start, POINT drop,
|
||||||
|
std::vector<ScreenshotMark> &marks, double tolerance)
|
||||||
|
{
|
||||||
|
assignMarkIds(marks);
|
||||||
|
auto &number=start?arrow.fromNumber:arrow.toNumber;
|
||||||
|
auto &link=start?arrow.fromLink:arrow.toLink;
|
||||||
|
number=0; link={};
|
||||||
|
if(!arrow.isLine()) return;
|
||||||
|
// Number badges are drawn above other objects.
|
||||||
|
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) {
|
||||||
|
number=it->number; return;
|
||||||
|
}
|
||||||
|
auto wouldCycle=[&](unsigned long long id) {
|
||||||
|
std::vector<unsigned long long> pending{id}; std::unordered_set<unsigned long long> seen;
|
||||||
|
while(!pending.empty()) {
|
||||||
|
auto current=pending.back(); pending.pop_back();
|
||||||
|
if(arrow.id && current==arrow.id) return true;
|
||||||
|
if(!current || !seen.insert(current).second) continue;
|
||||||
|
for(const auto &m:marks) if(m.id==current) {
|
||||||
|
pending.push_back(m.fromLink.target); pending.push_back(m.toLink.target); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
for(auto it=marks.rbegin();it!=marks.rend();++it) {
|
||||||
|
if(it->number || &*it==&arrow || (arrow.id && it->id==arrow.id)) continue;
|
||||||
|
ScreenshotLink candidate;
|
||||||
|
if(findObjectAnchor(*it,drop,tolerance,candidate) && !wouldCycle(it->id)) { link=candidate; return; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void resolveScreenshotLinks(std::vector<ScreenshotMark> &marks)
|
void resolveScreenshotLinks(std::vector<ScreenshotMark> &marks)
|
||||||
{
|
{
|
||||||
|
assignMarkIds(marks);
|
||||||
auto badge=[&](int number) -> const ScreenshotMark * {
|
auto badge=[&](int number) -> const ScreenshotMark * {
|
||||||
if(number) for(const auto &m:marks) if(m.number==number) return &m;
|
if(number) for(const auto &m:marks) if(m.number==number) return &m;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -1380,17 +1477,32 @@ void resolveScreenshotLinks(std::vector<ScreenshotMark> &marks)
|
|||||||
if(length<0.001) { dx=start?1:-1; dy=0; length=1; }
|
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))};
|
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)
|
std::vector<int> state(marks.size());
|
||||||
{
|
std::function<void(size_t)> resolve=[&](size_t index) {
|
||||||
if(m.number || m.isHighlight() || m.isText() || m.shape!=ScreenshotShape::None) continue;
|
if(state[index]) return;
|
||||||
auto from=badge(m.fromNumber),to=badge(m.toNumber);
|
state[index]=1;
|
||||||
if(!from) m.fromNumber=0;
|
auto &m=marks[index];
|
||||||
if(!to) m.toNumber=0;
|
if(m.isLine()) {
|
||||||
POINT start=from?from->from:m.from, end=to?to->from:m.to;
|
auto attach=[&](ScreenshotLink &link,POINT &point) {
|
||||||
if(from) m.from=rim(*from,end,true);
|
if(!link.target) return;
|
||||||
if(to) m.to=rim(*to,start,false);
|
for(size_t i=0;i<marks.size();++i) if(marks[i].id==link.target) {
|
||||||
if(m.freehand && !m.points.empty()) { m.points.front()=m.from; m.points.back()=m.to; }
|
if(state[i]==1) { link={}; return; } // Reject cyclic dependencies.
|
||||||
}
|
resolve(i); point=objectAnchor(marks[i],link); return;
|
||||||
|
}
|
||||||
|
link={}; // Deleted targets leave endpoints at their last position.
|
||||||
|
};
|
||||||
|
attach(m.fromLink,m.from); attach(m.toLink,m.to);
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
state[index]=2;
|
||||||
|
};
|
||||||
|
for(size_t i=0;i<marks.size();++i) resolve(i);
|
||||||
}
|
}
|
||||||
MarkPart hitScreenshotMark(const ScreenshotMark &m, POINT p, double tolerance)
|
MarkPart hitScreenshotMark(const ScreenshotMark &m, POINT p, double tolerance)
|
||||||
{
|
{
|
||||||
@@ -1458,6 +1570,7 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
|
|||||||
POINT position, int width, int height, bool snapRotation)
|
POINT position, int width, int height, bool snapRotation)
|
||||||
{
|
{
|
||||||
auto m=original;
|
auto m=original;
|
||||||
|
if(position.x==origin.x && position.y==origin.y) return m;
|
||||||
if(part==MarkPart::Rotation && (m.isText() || (m.isLine() && !m.label.empty()))) {
|
if(part==MarkPart::Rotation && (m.isText() || (m.isLine() && !m.label.empty()))) {
|
||||||
if(position.x==origin.x && position.y==origin.y) return m;
|
if(position.x==origin.x && position.y==origin.y) return m;
|
||||||
auto text=m.isText()?m:screenshotArrowLabel(m);
|
auto text=m.isText()?m:screenshotArrowLabel(m);
|
||||||
@@ -1493,6 +1606,7 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
|
|||||||
else if(part==MarkPart::Body)
|
else if(part==MarkPart::Body)
|
||||||
{
|
{
|
||||||
m.fromNumber=m.toNumber=0;
|
m.fromNumber=m.toNumber=0;
|
||||||
|
m.fromLink={}; m.toLink={};
|
||||||
auto shift=[&](LONG delta,LONG first,LONG last,int extent) {
|
auto shift=[&](LONG delta,LONG first,LONG last,int extent) {
|
||||||
LONG low=-first,high=LONG(extent-1)-last;
|
LONG low=-first,high=LONG(extent-1)-last;
|
||||||
if(low>high && m.isText()) {
|
if(low>high && m.isText()) {
|
||||||
@@ -1512,6 +1626,7 @@ ScreenshotMark dragScreenshotMark(const ScreenshotMark &original, MarkPart part,
|
|||||||
else if(part==MarkPart::Start || part==MarkPart::End)
|
else if(part==MarkPart::Start || part==MarkPart::End)
|
||||||
{
|
{
|
||||||
(part==MarkPart::Start?m.fromNumber:m.toNumber)=0;
|
(part==MarkPart::Start?m.fromNumber:m.toNumber)=0;
|
||||||
|
(part==MarkPart::Start?m.fromLink:m.toLink)={};
|
||||||
auto &endpoint=part==MarkPart::Start?m.from:m.to;
|
auto &endpoint=part==MarkPart::Start?m.from:m.to;
|
||||||
endpoint.x=std::clamp(endpoint.x+position.x-origin.x,0L,LONG(width-1));
|
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.y=std::clamp(endpoint.y+position.y-origin.y,0L,LONG(height-1));
|
||||||
|
|||||||
+8
-2
@@ -8,7 +8,7 @@ struct ScreenshotHost {
|
|||||||
HWND window{};
|
HWND window{};
|
||||||
unsigned windows{};
|
unsigned windows{};
|
||||||
bool (*notify)(const Clip &){};
|
bool (*notify)(const Clip &){};
|
||||||
bool autoOpenEditor = true;
|
bool autoOpenEditor = false;
|
||||||
};
|
};
|
||||||
inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72;
|
inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72;
|
||||||
void showScreenshot(ScreenshotHost &);
|
void showScreenshot(ScreenshotHost &);
|
||||||
@@ -17,6 +17,10 @@ enum class LineStyle { Solid, Dotted, Dashed, DashDot };
|
|||||||
enum class LineEnds { End, Start, Both, None };
|
enum class LineEnds { End, Start, Both, None };
|
||||||
enum class ScreenshotShape { None, Rectangle, Circle, Ellipse };
|
enum class ScreenshotShape { None, Rectangle, Circle, Ellipse };
|
||||||
struct ScreenshotOffset { double x, y; };
|
struct ScreenshotOffset { double x, y; };
|
||||||
|
struct ScreenshotLink {
|
||||||
|
unsigned long long target{};
|
||||||
|
double u{}, v{}; // Relative boundary position, or fractional position along a path.
|
||||||
|
};
|
||||||
struct ScreenshotMark {
|
struct ScreenshotMark {
|
||||||
POINT from, to; int number; int radius = 15; int thickness = 3;
|
POINT from, to; int number; int radius = 15; int thickness = 3;
|
||||||
int fromNumber = 0, toNumber = 0;
|
int fromNumber = 0, toNumber = 0;
|
||||||
@@ -33,6 +37,8 @@ struct ScreenshotMark {
|
|||||||
std::wstring label;
|
std::wstring label;
|
||||||
ScreenshotOffset labelOffset{0,-24}; // Along the line and perpendicular to it, in image pixels.
|
ScreenshotOffset labelOffset{0,-24}; // Along the line and perpendicular to it, in image pixels.
|
||||||
bool labelRotationManual=false;
|
bool labelRotationManual=false;
|
||||||
|
unsigned long long id{};
|
||||||
|
ScreenshotLink fromLink, toLink;
|
||||||
bool isText() const { return !text.empty(); }
|
bool isText() const { return !text.empty(); }
|
||||||
bool isHighlight() const { return !points.empty() && !freehand; }
|
bool isHighlight() const { return !points.empty() && !freehand; }
|
||||||
bool isLine() const { return !number && !isText() && !isHighlight() && shape==ScreenshotShape::None; }
|
bool isLine() const { return !number && !isText() && !isHighlight() && shape==ScreenshotShape::None; }
|
||||||
@@ -48,7 +54,7 @@ class ScreenshotHistory {
|
|||||||
};
|
};
|
||||||
void resolveScreenshotLinks(std::vector<ScreenshotMark> &);
|
void resolveScreenshotLinks(std::vector<ScreenshotMark> &);
|
||||||
void linkScreenshotEndpoint(ScreenshotMark &, bool start, POINT drop,
|
void linkScreenshotEndpoint(ScreenshotMark &, bool start, POINT drop,
|
||||||
const std::vector<ScreenshotMark> &, double tolerance);
|
std::vector<ScreenshotMark> &, double tolerance);
|
||||||
enum class MarkPart { None, Body, Start, End, Radius, Thickness, Rotation, Label };
|
enum class MarkPart { None, Body, Start, End, Radius, Thickness, Rotation, Label };
|
||||||
POINT screenshotThicknessHandle(const ScreenshotMark &, double clearance = 16);
|
POINT screenshotThicknessHandle(const ScreenshotMark &, double clearance = 16);
|
||||||
ScreenshotMark cloneScreenshotMark(const ScreenshotMark &, const std::vector<ScreenshotMark> &, int width, int height);
|
ScreenshotMark cloneScreenshotMark(const ScreenshotMark &, const std::vector<ScreenshotMark> &, int width, int height);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ LRESULT CALLBACK controller(HWND window,UINT message,WPARAM wp,LPARAM lp)
|
|||||||
int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
||||||
{
|
{
|
||||||
std::wstring token;
|
std::wstring token;
|
||||||
bool autoOpenEditor=true, writeLog=false;
|
bool autoOpenEditor=false, writeLog=false;
|
||||||
int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
|
int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
|
||||||
if(argc>1 && std::wstring(argv[argc-1])==L"--write-log") {
|
if(argc>1 && std::wstring(argv[argc-1])==L"--write-log") {
|
||||||
writeLog=true; --argc;
|
writeLog=true; --argc;
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
|||||||
s.undo = str(j, "undoHotkey", s.undo);
|
s.undo = str(j, "undoHotkey", s.undo);
|
||||||
s.clipboard = str(j, "clipboardHotkey", s.clipboard);
|
s.clipboard = str(j, "clipboardHotkey", s.clipboard);
|
||||||
s.screenshot = str(j, "screenshotHotkey", s.screenshot);
|
s.screenshot = str(j, "screenshotHotkey", s.screenshot);
|
||||||
s.screenshotAutoOpenEditor = j.value("screenshotAutoOpenEditor", true);
|
s.screenshotAutoOpenEditor = j.value("screenshotAutoOpenEditor", false);
|
||||||
if (j.contains("layouts"))
|
if (j.contains("layouts"))
|
||||||
{
|
{
|
||||||
s.cycle.clear();
|
s.cycle.clear();
|
||||||
|
|||||||
+2
-1
@@ -2,8 +2,9 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.2";
|
inline constexpr wchar_t AppVersion[] = L"0.6.5";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
struct UpdateResult
|
struct UpdateResult
|
||||||
{
|
{
|
||||||
std::wstring state, version, error;
|
std::wstring state, version, error;
|
||||||
|
|||||||
+82
-1
@@ -7,6 +7,20 @@
|
|||||||
namespace ks {
|
namespace ks {
|
||||||
struct InputRegressionAccess {
|
struct InputRegressionAccess {
|
||||||
static void setup(App &a) { a.hookBindings = {{Toggle,{MOD_CONTROL,VK_F12}}, {Case,{MOD_CONTROL|MOD_SHIFT,'U'}}}; }
|
static void setup(App &a) { a.hookBindings = {{Toggle,{MOD_CONTROL,VK_F12}}, {Case,{MOD_CONTROL|MOD_SHIFT,'U'}}}; }
|
||||||
|
static bool updateTimer(App &a) {
|
||||||
|
a.updateRunning=true; // Exercise timer scheduling without launching a network request.
|
||||||
|
App::windowProc(a.window,WM_TIMER,5,0);
|
||||||
|
bool first=KillTimer(a.window,5)!=FALSE;
|
||||||
|
App::windowProc(a.window,WM_TIMER,5,0);
|
||||||
|
bool repeated=KillTimer(a.window,5)!=FALSE;
|
||||||
|
a.updateRunning=false;
|
||||||
|
return first && repeated && !a.updateWorker.joinable();
|
||||||
|
}
|
||||||
|
static std::wstring updateResult(App &a,const wchar_t *state,const wchar_t *version) {
|
||||||
|
auto result=new UpdateResult; result->state=state; result->version=version;
|
||||||
|
App::windowProc(a.window,UpdateMessage,0,(LPARAM)result);
|
||||||
|
return a.notifiedUpdate;
|
||||||
|
}
|
||||||
static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); }
|
static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -31,6 +45,12 @@ int main(int argc, char **argv) {
|
|||||||
// Message-only window: no hooks, synthetic input, clipboard changes or visible editor.
|
// Message-only window: no hooks, synthetic input, clipboard changes or visible editor.
|
||||||
app.window=CreateWindowExW(0,L"STATIC",L"Input tests",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr);
|
app.window=CreateWindowExW(0,L"STATIC",L"Input tests",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr);
|
||||||
require(app.window!=nullptr,"message-only test window");
|
require(app.window!=nullptr,"message-only test window");
|
||||||
|
require(ks::UpdateCheckIntervalMs==21600000,"automatic update interval is six hours");
|
||||||
|
require(ks::InputRegressionAccess::updateTimer(app),"update timer rearms and avoids overlapping checks");
|
||||||
|
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.0")==L"99.0.0","background update records announced version");
|
||||||
|
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.0")==L"99.0.0","same available version remains deduplicated");
|
||||||
|
require(ks::InputRegressionAccess::updateResult(app,L"error",L"")==L"99.0.0","network error preserves announced version");
|
||||||
|
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.1")==L"99.0.1","new version can be announced after an error");
|
||||||
for(int variant=0;variant<3;++variant) for(int icon=0;icon<15;++icon)
|
for(int variant=0;variant<3;++variant) for(int icon=0;icon<15;++icon)
|
||||||
for(int size:{22,44,55}) {
|
for(int size:{22,44,55}) {
|
||||||
auto handle=(HICON)LoadImageW(app.instance,MAKEINTRESOURCEW(icon<9?300+variant*10+icon:400+variant*10+icon-9),IMAGE_ICON,size,size,0);
|
auto handle=(HICON)LoadImageW(app.instance,MAKEINTRESOURCEW(icon<9?300+variant*10+icon:400+variant*10+icon-9),IMAGE_ICON,size,size,0);
|
||||||
@@ -159,6 +179,67 @@ int main(int argc, char **argv) {
|
|||||||
require(detached.fromNumber==0,"dropping away from badges remains detached");
|
require(detached.fromNumber==0,"dropping away from badges remains detached");
|
||||||
auto translated=ks::dragScreenshotMark(linked[2],ks::MarkPart::Body,{80,35},{85,40},160,100);
|
auto translated=ks::dragScreenshotMark(linked[2],ks::MarkPart::Body,{80,35},{85,40},160,100);
|
||||||
require(translated.fromNumber==0 && translated.toNumber==0,"moving whole arrow detaches both endpoints");
|
require(translated.fromNumber==0 && translated.toNumber==0,"moving whole arrow detaches both endpoints");
|
||||||
|
for(auto shape : {ks::ScreenshotShape::Rectangle,ks::ScreenshotShape::Circle,ks::ScreenshotShape::Ellipse}) {
|
||||||
|
std::vector<ks::ScreenshotMark> objects={{{100,50},{160,110},0},{{10,80},{100,80},0},{{10,140},{55,80},0}};
|
||||||
|
objects[0].shape=shape;
|
||||||
|
ks::linkScreenshotEndpoint(objects[1],false,{100,80},objects,6);
|
||||||
|
ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects[1].toLink.target==objects[0].id && objects[1].to.x==100,"arrow attaches to shape boundary");
|
||||||
|
ks::linkScreenshotEndpoint(objects[2],false,{55,80},objects,6);
|
||||||
|
ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects[2].toLink.target==objects[1].id,"arrow attaches to another arrow body");
|
||||||
|
auto targetId=objects[0].id,arrowId=objects[1].id;
|
||||||
|
auto snapshot=objects; ks::ScreenshotHistory linksHistory; linksHistory.remember(objects);
|
||||||
|
// Reverse storage order so dependency resolution cannot rely on insertion order.
|
||||||
|
std::reverse(objects.begin(),objects.end());
|
||||||
|
objects[2]=ks::dragScreenshotMark(objects[2],ks::MarkPart::Body,{0,0},{20,20},400,300);
|
||||||
|
ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects[1].to.x==120 && objects[1].to.y==100 && objects[0].to.x==65 && objects[0].to.y==90,
|
||||||
|
"shape-arrow-arrow chain updates in dependency order");
|
||||||
|
require(linksHistory.undo(objects) && objects[1].to.x==100 && objects[2].toLink.target==arrowId,
|
||||||
|
"undo restores geometry and generic links");
|
||||||
|
require(linksHistory.redo(objects) && objects[0].to.x==65,"redo restores moved chain");
|
||||||
|
auto resized=objects;
|
||||||
|
auto corner=resized[2].to;
|
||||||
|
resized[2]=ks::dragScreenshotMark(resized[2],ks::MarkPart::End,corner,{corner.x+20,corner.y+20},400,300);
|
||||||
|
ks::resolveScreenshotLinks(resized);
|
||||||
|
require(resized[1].to.x==120 && resized[1].to.y==110 && resized[0].to.y==95,
|
||||||
|
"resizing shapes updates both levels of attached arrows");
|
||||||
|
ks::linkScreenshotEndpoint(objects[1],true,objects[0].from,objects,6);
|
||||||
|
require(!objects[1].fromLink.target,"cyclic arrow links are rejected");
|
||||||
|
auto clone=ks::cloneScreenshotMark(objects[1],objects,400,300);
|
||||||
|
require(!clone.id && !clone.fromLink.target && !clone.toLink.target,"cloned connector has independent identity and no links");
|
||||||
|
auto endpoint=objects[1].to;
|
||||||
|
objects.erase(objects.begin()+2); ks::resolveScreenshotLinks(objects);
|
||||||
|
require(!objects[1].toLink.target && objects[1].to.x==endpoint.x && objects[0].toLink.target==arrowId,
|
||||||
|
"deleting shape detaches only its dependents without moving endpoints");
|
||||||
|
objects.push_back(snapshot[0]); objects.back().id=0; ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects.back().id!=targetId && !objects[1].toLink.target,"new objects cannot revive deleted links");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::vector<ks::ScreenshotMark> objects={{{80,40},{80,40},0},{{10,40},{80,40},0}};
|
||||||
|
objects[0].text=L"Target"; objects[0].fontSize=20;
|
||||||
|
auto bounds=ks::screenshotTextBounds(objects[0]);
|
||||||
|
ks::linkScreenshotEndpoint(objects[1],false,{bounds.left,(bounds.top+bounds.bottom)/2},objects,6);
|
||||||
|
ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects[1].toLink.target==objects[0].id,"arrow attaches to text block");
|
||||||
|
auto old=objects[1].to;
|
||||||
|
objects[0]=ks::dragScreenshotMark(objects[0],ks::MarkPart::Body,{0,0},{30,20},400,300);
|
||||||
|
ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects[1].to.x==old.x+30 && objects[1].to.y==old.y+20,"text attachment follows movement");
|
||||||
|
objects[0].rotation=90; ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects[1].to.x!=old.x+30 || objects[1].to.y!=old.y+20,"text attachment follows rotation");
|
||||||
|
}
|
||||||
|
for(bool freehand : {false,true}) {
|
||||||
|
std::vector<ks::ScreenshotMark> objects={{{80,60},{160,80},0},{{10,140},{110,60},0}};
|
||||||
|
objects[0].points={{80,60},{140,60},{160,80}}; objects[0].freehand=freehand;
|
||||||
|
ks::linkScreenshotEndpoint(objects[1],false,{110,60},objects,6);
|
||||||
|
ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects[1].toLink.target==objects[0].id,"arrow attaches to freehand and highlight strokes");
|
||||||
|
objects[0]=ks::dragScreenshotMark(objects[0],ks::MarkPart::Body,{0,0},{20,20},400,300);
|
||||||
|
ks::resolveScreenshotLinks(objects);
|
||||||
|
require(objects[1].to.x==130 && objects[1].to.y==80,"stroke attachment follows the original segment");
|
||||||
|
}
|
||||||
auto lastPoint=linked[2].from;
|
auto lastPoint=linked[2].from;
|
||||||
linked.erase(linked.begin());
|
linked.erase(linked.begin());
|
||||||
ks::resolveScreenshotLinks(linked);
|
ks::resolveScreenshotLinks(linked);
|
||||||
@@ -509,7 +590,7 @@ int main(int argc, char **argv) {
|
|||||||
require(EqualRect(&listBefore,&(listAfter=childRect(102))),"DPI refresh preserves history list geometry");
|
require(EqualRect(&listBefore,&(listAfter=childRect(102))),"DPI refresh preserves history list geometry");
|
||||||
}
|
}
|
||||||
auto settings=app.settingsWindow;
|
auto settings=app.settingsWindow;
|
||||||
require(SendDlgItemMessageW(settings,209,BM_GETCHECK,0,0)==BST_CHECKED,"automatic screenshot editor checkbox defaults on");
|
require(SendDlgItemMessageW(settings,209,BM_GETCHECK,0,0)==BST_UNCHECKED,"automatic screenshot editor checkbox defaults off");
|
||||||
require(settings&&!IsWindowVisible(settings),"settings layout tested without changing user focus");
|
require(settings&&!IsWindowVisible(settings),"settings layout tested without changing user focus");
|
||||||
for(auto pair:{std::pair{209,215},std::pair{209,207},std::pair{209,220},std::pair{209,232},std::pair{207,215},std::pair{207,232},std::pair{207,220},std::pair{232,230},std::pair{230,231},std::pair{231,253}}) {
|
for(auto pair:{std::pair{209,215},std::pair{209,207},std::pair{209,220},std::pair{209,232},std::pair{207,215},std::pair{207,232},std::pair{207,220},std::pair{232,230},std::pair{230,231},std::pair{231,253}}) {
|
||||||
RECT first{},second{},overlap{};
|
RECT first{},second{},overlap{};
|
||||||
|
|||||||
@@ -94,6 +94,56 @@ int main()
|
|||||||
c.hoveredPart=ks::MarkPart::Rotation; ks::updateStatus(c);
|
c.hoveredPart=ks::MarkPart::Rotation; ks::updateStatus(c);
|
||||||
require(c.statusText.find(L"15-degree")!=std::wstring::npos,"rotation hint explains Shift snapping");
|
require(c.statusText.find(L"15-degree")!=std::wstring::npos,"rotation hint explains Shift snapping");
|
||||||
c.hoveredPart=ks::MarkPart::None;
|
c.hoveredPart=ks::MarkPart::None;
|
||||||
|
// Drag the two badges in turn, including a click near the attached endpoint.
|
||||||
|
for(bool freehand : {false,true}) for(auto ends : {ks::LineEnds::End,ks::LineEnds::None}) {
|
||||||
|
c.marks={{{60,80},{60,80},1},{{230,80},{230,80},2},{{60,80},{230,80},0}};
|
||||||
|
auto &connector=c.marks[2]; connector.fromNumber=1; connector.toNumber=2;
|
||||||
|
connector.ends=ends; connector.freehand=freehand;
|
||||||
|
if(freehand) connector.points={{60,80},{140,80},{230,80}};
|
||||||
|
ks::resolveScreenshotLinks(c.marks); c.selected=-1; c.selectedLabel=false;
|
||||||
|
auto mouse=[&](UINT message,POINT p) {
|
||||||
|
LONG x=c.view.left+MulDiv(p.x,c.view.right-c.view.left,c.image->width);
|
||||||
|
LONG 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));
|
||||||
|
};
|
||||||
|
for(int badge : {0,1,0,1}) {
|
||||||
|
auto origin=c.marks[badge].from;
|
||||||
|
// The second badge's inner edge overlaps the connector endpoint hit area.
|
||||||
|
if(badge==1) origin.x-=12;
|
||||||
|
mouse(WM_LBUTTONDOWN,origin);
|
||||||
|
require(c.selected==badge && c.part==ks::MarkPart::Body,"click on linked badge selects badge instead of connector endpoint");
|
||||||
|
mouse(WM_MOUSEMOVE,{origin.x,origin.y+20});
|
||||||
|
mouse(WM_LBUTTONUP,{origin.x,origin.y+20});
|
||||||
|
require(c.marks[2].fromNumber==1 && c.marks[2].toNumber==2,"alternating badge drags preserve both connector links");
|
||||||
|
auto start=c.marks[0].from,end=c.marks[1].from;
|
||||||
|
require(std::abs(std::hypot(double(c.marks[2].from.x-start.x),double(c.marks[2].from.y-start.y))-15)<1 &&
|
||||||
|
std::abs(std::hypot(double(c.marks[2].to.x-end.x),double(c.marks[2].to.y-end.y))-15)<1,
|
||||||
|
"connector endpoints follow both badge rims");
|
||||||
|
}
|
||||||
|
// Selecting a linked connector (also the first click of a double-click)
|
||||||
|
// must not detach it before opening its caption editor.
|
||||||
|
for(int entry : {0,1,2}) {
|
||||||
|
c.selected=-1;
|
||||||
|
POINT body{110,c.marks[0].from.y};
|
||||||
|
if(freehand) body={(c.marks[2].points[0].x+c.marks[2].points[1].x)/2,
|
||||||
|
(c.marks[2].points[0].y+c.marks[2].points[1].y)/2};
|
||||||
|
mouse(WM_LBUTTONDOWN,body); mouse(WM_LBUTTONUP,body);
|
||||||
|
require(c.selected==2 && c.marks[2].fromNumber==1 && c.marks[2].toNumber==2,
|
||||||
|
"clicking a connector without moving preserves both links");
|
||||||
|
if(entry==0) { mouse(WM_LBUTTONDBLCLK,body); mouse(WM_LBUTTONUP,body); }
|
||||||
|
else if(entry==1) SendMessageW(window,WM_KEYDOWN,VK_F2,0);
|
||||||
|
else SendMessageW(window,WM_COMMAND,35,0);
|
||||||
|
require(c.inlineText && c.editingLabel,"linked connector caption opens through every entry point");
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Linked caption");
|
||||||
|
ks::finishInlineText(window,c,true);
|
||||||
|
require(c.marks[2].label==L"Linked caption" && c.marks[2].fromNumber==1 && c.marks[2].toNumber==2,
|
||||||
|
"adding or editing a caption preserves both connector links");
|
||||||
|
}
|
||||||
|
c.selected=2;
|
||||||
|
require(ks::markAt(c,c.marks[1].from).first==1,"selected connector cannot steal a badge click");
|
||||||
|
auto outside=c.marks[2].to; outside.x-=4;
|
||||||
|
require(ks::markAt(c,outside)==std::pair{2,ks::MarkPart::End},"endpoint remains draggable outside badge");
|
||||||
|
}
|
||||||
std::vector<ks::Mark> nudgeMarks;
|
std::vector<ks::Mark> nudgeMarks;
|
||||||
nudgeMarks.push_back({{60,60},{60,60},1});
|
nudgeMarks.push_back({{60,60},{60,60},1});
|
||||||
nudgeMarks.push_back({{60,60},{120,100},0});
|
nudgeMarks.push_back({{60,60},{120,100},0});
|
||||||
@@ -165,8 +215,12 @@ int main()
|
|||||||
require(!IsWindow(window),"copied screenshot closes without confirmation");
|
require(!IsWindow(window),"copied screenshot closes without confirmation");
|
||||||
static bool published=false;
|
static bool published=false;
|
||||||
static bool notificationAccepted=false;
|
static bool notificationAccepted=false;
|
||||||
|
static HWND notificationWindow{};
|
||||||
|
static bool expectHidden=false;
|
||||||
app.notify=[](const ks::Clip &) {
|
app.notify=[](const ks::Clip &) {
|
||||||
if(!published) throw std::runtime_error("notification preceded clipboard publication");
|
if(!published) throw std::runtime_error("notification preceded clipboard publication");
|
||||||
|
if(expectHidden && IsWindowVisible(notificationWindow))
|
||||||
|
throw std::runtime_error("selection overlay is still visible when notification is sent");
|
||||||
return notificationAccepted;
|
return notificationAccepted;
|
||||||
};
|
};
|
||||||
for(bool autoOpen : {true,false}) for (bool accepted : {true, false}) {
|
for(bool autoOpen : {true,false}) for (bool accepted : {true, false}) {
|
||||||
@@ -178,6 +232,8 @@ int main()
|
|||||||
PatBlt(selection.image->dc,0,0,100,100,WHITENESS);
|
PatBlt(selection.image->dc,0,0,100,100,WHITENESS);
|
||||||
selection.start={10,20}; selection.end={70,60};
|
selection.start={10,20}; selection.end={70,60};
|
||||||
auto overlay=CreateWindowW(cls.lpszClassName,L"Selection test",WS_POPUP,0,0,100,100,app.window,nullptr,app.instance,&selection);
|
auto overlay=CreateWindowW(cls.lpszClassName,L"Selection test",WS_POPUP,0,0,100,100,app.window,nullptr,app.instance,&selection);
|
||||||
|
notificationWindow=overlay; expectHidden=!autoOpen;
|
||||||
|
ShowWindow(overlay,SW_SHOWNOACTIVATE);
|
||||||
ks::finishSelection(overlay,selection,[](HWND,const ks::Clip &clip)->DWORD {
|
ks::finishSelection(overlay,selection,[](HWND,const ks::Clip &clip)->DWORD {
|
||||||
auto pixels=ks::decodeImage(clip);
|
auto pixels=ks::decodeImage(clip);
|
||||||
if(pixels.width!=60 || pixels.height!=40) throw std::runtime_error("automatic copy did not crop selection");
|
if(pixels.width!=60 || pixels.height!=40) throw std::runtime_error("automatic copy did not crop selection");
|
||||||
|
|||||||
+4
-1
@@ -430,7 +430,10 @@ int main(int argc, char **argv)
|
|||||||
auto invalid = settings;
|
auto invalid = settings;
|
||||||
auto captureSettings = settings;
|
auto captureSettings = settings;
|
||||||
captureSettings.screenshot = L"Ctrl+Shift+F9";
|
captureSettings.screenshot = L"Ctrl+Shift+F9";
|
||||||
require(captureSettings.screenshotAutoOpenEditor,"editor opens by default");
|
require(!captureSettings.screenshotAutoOpenEditor,"notification-first capture is the default");
|
||||||
|
captureSettings.screenshotAutoOpenEditor=true;
|
||||||
|
require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor,
|
||||||
|
"explicit automatic editor preference survives settings roundtrip");
|
||||||
captureSettings.screenshotAutoOpenEditor=false;
|
captureSettings.screenshotAutoOpenEditor=false;
|
||||||
require(!ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor,
|
require(!ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor,
|
||||||
"notification-first preference survives settings roundtrip");
|
"notification-first preference survives settings roundtrip");
|
||||||
|
|||||||
Reference in New Issue
Block a user