Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fedb54dfe | |||
| bbf20e8270 | |||
| b8bb14a985 |
@@ -90,8 +90,11 @@ broker can load them even when the profile directory is inaccessible to it.
|
|||||||
Notifications expire after 24 hours. Cache cleanup runs on capture/open; each cache is bounded to
|
Notifications expire after 24 hours. Cache cleanup runs on capture/open; each cache is bounded to
|
||||||
40 files and 256 MiB. Expired or evicted notifications cannot reopen their screenshot.
|
40 files and 256 MiB. Expired or evicted notifications cannot reopen their screenshot.
|
||||||
In the editor, **Ctrl+C** or **Enter** copies the current result without closing the window.
|
In the editor, **Ctrl+C** or **Enter** copies the current result without closing the window.
|
||||||
|
The editor starts with **Pointer (V)**: select objects, move them, and edit geometry, thickness and
|
||||||
|
other properties using handles and controls. Clicking empty space clears selection; dragging empty
|
||||||
|
space adds nothing. Choose a drawing tool to create objects, then return to **V** to edit them.
|
||||||
To annotate: **N** places sequential numbered red markers with a click, and **A** draws
|
To annotate: **N** places sequential numbered red markers with a click, and **A** draws
|
||||||
red arrows by dragging from the tail to the tip. Click an existing marker or arrow to select it;
|
red arrows by dragging from the tail to the tip. With **Pointer (V)**, click an existing marker or arrow to select it;
|
||||||
drag its body to move it. Drag the square handle on a number's right edge to change its diameter
|
drag its body to move it. Drag the square handle on a number's right edge to change its diameter
|
||||||
(the text scales with it), or either endpoint handle of an arrow to change its length and direction.
|
(the text scales with it), or either endpoint handle of an arrow to change its length and direction.
|
||||||
Select an arrow and drag the extra handle beside its midpoint perpendicular to the line to change
|
Select an arrow and drag the extra handle beside its midpoint perpendicular to the line to change
|
||||||
@@ -101,6 +104,14 @@ 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. In Arrow or Straight line mode, hold **Ctrl** and drag from one object
|
||||||
|
to another to draw over them and automatically attach both endpoints. Without Ctrl, existing objects
|
||||||
|
can still be selected and edited as before. With Pointer, drag an existing endpoint onto a target to attach it. 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
|
||||||
@@ -129,6 +140,8 @@ The mouse wheel over text adjusts its font size. Text supports color, dragging,
|
|||||||
Delete and Undo/Redo. Arrow keys move the selected object by one source-image pixel;
|
Delete and Undo/Redo. Arrow keys move the selected object by one source-image pixel;
|
||||||
selecting an arrow caption moves only that caption. Repositioning a caption preserves its
|
selecting an arrow caption moves only that caption. Repositioning a caption preserves its
|
||||||
automatic angle; only dragging the rotation handle switches to a fixed angle.
|
automatic angle; only dragging the rotation handle switches to a fixed angle.
|
||||||
|
New standalone text starts horizontally, even after editing an angled arrow caption. Font styling
|
||||||
|
is reused; rotation belongs to each individual object.
|
||||||
Text has a rotation handle above its selection. Drag to rotate; hold Shift to snap to 15-degree steps.
|
Text has a rotation handle above its selection. Drag to rotate; hold Shift to snap to 15-degree steps.
|
||||||
Double-click an arrow, press F2, or use the label icon to add/edit its caption. Captions follow
|
Double-click an arrow, press F2, or use the label icon to add/edit its caption. Captions follow
|
||||||
the arrow when moved or resized; drag the caption to adjust its offset. By default, the caption
|
the arrow when moved or resized; drag the caption to adjust its offset. By default, the caption
|
||||||
|
|||||||
Binary file not shown.
+7
-10
@@ -1,17 +1,14 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.3 — 2026-10-29
|
## v0.6.6 — 2026-10-29
|
||||||
|
|
||||||
- Hide the fullscreen capture selection before submitting a notification when automatic editor opening is disabled. Previously the selection remained visible during submission, which could suppress the banner under Windows fullscreen notification rules.
|
## Changes
|
||||||
- Default to capture → notification → click → editor. **Open editor after screen capture** is now off by default; explicitly saved preferences are preserved.
|
- Add **Pointer (V)** as the initial editor tool. Select and move objects, edit geometry and thickness with handles, and clear selection by clicking empty space. Pointer never creates objects on empty space.
|
||||||
- Retain the editor fallback for immediate notification errors.
|
- Preserve **Ctrl+drag** for drawing over existing objects and automatically linking connector endpoints. Add clearer Ctrl hints to the Arrow and Straight line buttons and status bar.
|
||||||
- Check for updates in the background every 6 hours after the startup check. Notify once per version per session, retry after network failures at the next interval, and install only after confirmation.
|
- Fix new standalone text inheriting the angle of an arrow caption. New text reuses typography only and starts horizontally; editing existing rotated text preserves its own angle.
|
||||||
|
|
||||||
Verified: 176 native checks, 403 input/layout checks at 240 DPI, screenshot editor UI integration,
|
## Validation
|
||||||
isolated notification registration checks, and 45 updater checks. The new UI regression test failed
|
176 native checks, 440 input/layout checks at 240 DPI, screenshot editor UI integration, isolated notification registration, and 45 updater checks. UI coverage includes Pointer interactions, Ctrl drawing between markers/shapes/arrows, and standalone text after automatic or manually rotated arrow captions.
|
||||||
before the fix and passes afterward: the selection is hidden at notification submission.
|
|
||||||
Banner behavior still needs confirmation on the affected laptop. Windows Do Not Disturb can
|
|
||||||
send notifications directly to the notification center; asynchronous toast failures are not yet monitored.
|
|
||||||
|
|
||||||
**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 @@
|
|||||||
a43786b06878da0f069dbad0fa29587a736dd2d8904c7bc991f5cf0c0570a16f KSwitcher-win-x64.zip
|
cb711ace317e60054f6498436f02bd66f10adebfe9362b8d1c106569cd27a600 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,3,0
|
FILEVERSION 0,6,6,0
|
||||||
PRODUCTVERSION 0,6,3,0
|
PRODUCTVERSION 0,6,6,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.3"
|
VALUE "FileVersion", "0.6.6"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+156
-31
@@ -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>
|
||||||
@@ -51,7 +53,7 @@ bool confirmScreenshotClose(HWND owner)
|
|||||||
// If Windows cannot display the confirmation, keep the screenshot open.
|
// If Windows cannot display the confirmation, keep the screenshot open.
|
||||||
return SUCCEEDED(result) && button==IDYES;
|
return SUCCEEDED(result) && button==IDYES;
|
||||||
}
|
}
|
||||||
constexpr int ToolIds[]{10,11,22,23,24,25,21,19,26,20,12,18,13};
|
constexpr int ToolIds[]{27,10,11,22,23,24,25,21,19,26,20,12,18,13};
|
||||||
struct Surface
|
struct Surface
|
||||||
{
|
{
|
||||||
HDC dc = CreateCompatibleDC(nullptr);
|
HDC dc = CreateCompatibleDC(nullptr);
|
||||||
@@ -129,7 +131,7 @@ struct Capture
|
|||||||
bool editing{}, dragging{}, arrow{}, owned{}, copied{};
|
bool editing{}, dragging{}, arrow{}, owned{}, copied{};
|
||||||
bool autoOpenEditor;
|
bool autoOpenEditor;
|
||||||
bool highlight{};
|
bool highlight{};
|
||||||
bool freehand=true;
|
bool pointer=true, freehand=false;
|
||||||
bool straight{}, textTool{};
|
bool straight{}, textTool{};
|
||||||
Mark textDefaults{};
|
Mark textDefaults{};
|
||||||
ScreenshotShape shape=ScreenshotShape::None;
|
ScreenshotShape shape=ScreenshotShape::None;
|
||||||
@@ -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)
|
||||||
@@ -205,11 +207,12 @@ void updateStatus(Capture &c)
|
|||||||
else if(m.isText()) text=L"Text: arrows = 1 px | F2 = edit | Round handle = rotate; Shift = 15 degrees";
|
else if(m.isText()) text=L"Text: arrows = 1 px | F2 = edit | Round handle = rotate; Shift = 15 degrees";
|
||||||
else text=L"Arrows = 1 px | Del = delete | Ctrl+D = clone | Ctrl+Z / Ctrl+Y = undo / redo";
|
else text=L"Arrows = 1 px | Del = delete | Ctrl+D = clone | Ctrl+Z / Ctrl+Y = undo / redo";
|
||||||
}
|
}
|
||||||
|
else if(c.pointer) text=L"Pointer (V): select and edit objects | Drag handles = geometry or thickness | Empty space clears selection";
|
||||||
else if(c.textTool) text=L"Text (T): click the image to type | Double-click existing text to edit";
|
else if(c.textTool) text=L"Text (T): click the image to type | Double-click existing text to edit";
|
||||||
else if(c.highlight) text=L"Highlight (H): drag to paint | Ctrl+drag = draw over an object";
|
else if(c.highlight) text=L"Highlight (H): drag to paint | Ctrl+drag = paint over an object | V = pointer";
|
||||||
else if(c.freehand) text=L"Freehand (F): drag to draw | Ctrl+drag = draw over an object";
|
else if(c.freehand) text=L"Freehand (F): drag to draw | Ctrl+drag = draw over an object | V = pointer";
|
||||||
else if(c.arrow) text=L"Drag to draw | Ctrl+drag = draw over an object | F2 on a line = caption";
|
else if(c.arrow) text=L"Drag to draw | Ctrl+drag over objects = linked connector | V = pointer";
|
||||||
else text=L"Number (N): click to add | Drag objects to move | Ctrl+C = copy screenshot";
|
else text=L"Number (N): click to add | V = select and edit | Ctrl+C = copy screenshot";
|
||||||
if(text!=c.statusText) {
|
if(text!=c.statusText) {
|
||||||
c.statusText=std::move(text);
|
c.statusText=std::move(text);
|
||||||
SendMessageW(status,SB_SETTEXTW,0,(LPARAM)c.statusText.c_str());
|
SendMessageW(status,SB_SETTEXTW,0,(LPARAM)c.statusText.c_str());
|
||||||
@@ -250,6 +253,15 @@ void positionInlineText(Capture &c)
|
|||||||
int height=std::min(std::max(40,MulDiv(bounds.bottom-bounds.top,viewWidth,c.image->width)+24),std::max(32,int(client.bottom)-c.statusHeight-y));
|
int height=std::min(std::max(40,MulDiv(bounds.bottom-bounds.top,viewWidth,c.image->width)+24),std::max(32,int(client.bottom)-c.statusHeight-y));
|
||||||
SetWindowPos(c.inlineText,nullptr,x-2,y-2,width,height,SWP_NOZORDER|SWP_NOACTIVATE);
|
SetWindowPos(c.inlineText,nullptr,x-2,y-2,width,height,SWP_NOZORDER|SWP_NOACTIVATE);
|
||||||
}
|
}
|
||||||
|
Mark textStyleDefaults(const Mark &source)
|
||||||
|
{
|
||||||
|
// Reuse typography only, never an object's angle, position, identity or links.
|
||||||
|
Mark style{};
|
||||||
|
style.fontName=source.fontName; style.fontSize=source.fontSize;
|
||||||
|
style.fontWeight=source.fontWeight; style.fontItalic=source.fontItalic;
|
||||||
|
style.color=source.color;
|
||||||
|
return style;
|
||||||
|
}
|
||||||
void finishInlineText(HWND w,Capture &c,bool commit)
|
void finishInlineText(HWND w,Capture &c,bool commit)
|
||||||
{
|
{
|
||||||
if(!c.inlineText) return;
|
if(!c.inlineText) return;
|
||||||
@@ -272,8 +284,9 @@ void finishInlineText(HWND w,Capture &c,bool commit)
|
|||||||
} else if(!empty) {
|
} else if(!empty) {
|
||||||
c.remember(c.marks); c.marks.push_back(c.textDraft); c.selected=int(c.marks.size())-1; c.selectedLabel=false;
|
c.remember(c.marks); c.marks.push_back(c.textDraft); c.selected=int(c.marks.size())-1; c.selectedLabel=false;
|
||||||
}
|
}
|
||||||
if(!empty) c.textDefaults=c.textDraft;
|
if(!empty) c.textDefaults=textStyleDefaults(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);
|
||||||
}
|
}
|
||||||
@@ -536,7 +549,7 @@ void layout(HWND w, Capture &c)
|
|||||||
RECT statusRect{}; GetWindowRect(status,&statusRect);
|
RECT statusRect{}; GetWindowRect(status,&statusRect);
|
||||||
c.statusHeight=statusRect.bottom-statusRect.top;
|
c.statusHeight=statusRect.bottom-statusRect.top;
|
||||||
r.bottom-=c.statusHeight;
|
r.bottom-=c.statusHeight;
|
||||||
const int positions[]{8,44,80,116,152,188,224,260,296,332,380,416,464};
|
const int positions[]{8,44,80,116,152,188,224,260,296,332,368,416,452,500};
|
||||||
static_assert(std::size(positions)==std::size(ToolIds));
|
static_assert(std::size(positions)==std::size(ToolIds));
|
||||||
for (size_t i = 0; i < std::size(ToolIds); ++i)
|
for (size_t i = 0; i < std::size(ToolIds); ++i)
|
||||||
{
|
{
|
||||||
@@ -575,6 +588,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];
|
||||||
@@ -598,7 +620,7 @@ std::pair<int,MarkPart> markAt(Capture &c, POINT p)
|
|||||||
}
|
}
|
||||||
void captureCursor(HWND w, Capture &c)
|
void captureCursor(HWND w, Capture &c)
|
||||||
{
|
{
|
||||||
LPCWSTR cursor=IDC_CROSS;
|
LPCWSTR cursor=c.editing && c.pointer?IDC_ARROW:IDC_CROSS;
|
||||||
if(c.editing)
|
if(c.editing)
|
||||||
{
|
{
|
||||||
POINT p; GetCursorPos(&p); ScreenToClient(w,&p);
|
POINT p; GetCursorPos(&p); ScreenToClient(w,&p);
|
||||||
@@ -686,7 +708,7 @@ void drawTool(const DRAWITEMSTRUCT &item, const Capture &c)
|
|||||||
{
|
{
|
||||||
auto dc=item.hDC;
|
auto dc=item.hDC;
|
||||||
RECT r=item.rcItem;
|
RECT r=item.rcItem;
|
||||||
bool active=(item.CtlID==10 && !c.arrow && !c.highlight && !c.freehand && !c.textTool)||(item.CtlID==11 && c.arrow && !c.straight && c.shape==ScreenshotShape::None)||
|
bool active=(item.CtlID==27 && c.pointer)||(item.CtlID==10 && !c.pointer && !c.arrow && !c.highlight && !c.freehand && !c.textTool)||(item.CtlID==11 && c.arrow && !c.straight && c.shape==ScreenshotShape::None)||
|
||||||
(item.CtlID==19 && c.highlight)||(item.CtlID==21 && c.freehand)||
|
(item.CtlID==19 && c.highlight)||(item.CtlID==21 && c.freehand)||
|
||||||
(item.CtlID==22 && c.straight)||(item.CtlID>=23 && item.CtlID<=25 && int(c.shape)==int(item.CtlID)-22)||(item.CtlID==26 && c.textTool);
|
(item.CtlID==22 && c.straight)||(item.CtlID>=23 && item.CtlID<=25 && int(c.shape)==int(item.CtlID)-22)||(item.CtlID==26 && c.textTool);
|
||||||
bool disabled=item.itemState&ODS_DISABLED;
|
bool disabled=item.itemState&ODS_DISABLED;
|
||||||
@@ -702,7 +724,7 @@ void drawTool(const DRAWITEMSTRUCT &item, const Capture &c)
|
|||||||
RoundRect(dc,r.left,r.top,r.right,r.bottom,corner,corner);
|
RoundRect(dc,r.left,r.top,r.right,r.bottom,corner,corner);
|
||||||
SelectObject(dc,oldPen); SelectObject(dc,oldBrush); DeleteObject(pen); DeleteObject(brush);
|
SelectObject(dc,oldPen); SelectObject(dc,oldBrush); DeleteObject(pen); DeleteObject(brush);
|
||||||
int iconIndex=item.CtlID==35?13:item.CtlID==34?14:item.CtlID>=22?int(item.CtlID)-13:item.CtlID==18?5:item.CtlID==19?6:item.CtlID==20?7:item.CtlID==21?8:item.CtlID-10;
|
int iconIndex=item.CtlID==35?13:item.CtlID==34?14:item.CtlID>=22?int(item.CtlID)-13:item.CtlID==18?5:item.CtlID==19?6:item.CtlID==20?7:item.CtlID==21?8:item.CtlID-10;
|
||||||
auto icon=c.toolIcons[disabled?2:active?1:0][iconIndex];
|
auto icon=item.CtlID==27?(HICON)LoadCursorW(nullptr,IDC_ARROW):c.toolIcons[disabled?2:active?1:0][iconIndex];
|
||||||
if(icon) DrawIconEx(dc,(r.right-c.iconSize)/2,(r.bottom-c.iconSize)/2,icon,c.iconSize,c.iconSize,0,nullptr,DI_NORMAL);
|
if(icon) DrawIconEx(dc,(r.right-c.iconSize)/2,(r.bottom-c.iconSize)/2,icon,c.iconSize,c.iconSize,0,nullptr,DI_NORMAL);
|
||||||
if(item.CtlID==20) {
|
if(item.CtlID==20) {
|
||||||
auto color=c.selected>=0?c.marks[c.selected].color:c.currentColor;
|
auto color=c.selected>=0?c.marks[c.selected].color:c.currentColor;
|
||||||
@@ -763,7 +785,7 @@ void edit(HWND w, Capture &c)
|
|||||||
InitCommonControlsEx(&statusControls);
|
InitCommonControlsEx(&statusControls);
|
||||||
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
|
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
|
||||||
0,0,0,0,w,(HMENU)50,c.app.instance,nullptr);
|
0,0,0,0,w,(HMENU)50,c.app.instance,nullptr);
|
||||||
const wchar_t *labels[]{L"Number (N)", L"Arrow (A)", L"Straight line (L)", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Ctrl+drag to draw over an object", L"Highlight (H); Ctrl+drag to paint over an object",
|
const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Ctrl+drag to draw over an object", L"Highlight (H); Ctrl+drag to paint over an object",
|
||||||
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
|
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
|
||||||
static_assert(std::size(labels)==std::size(ToolIds));
|
static_assert(std::size(labels)==std::size(ToolIds));
|
||||||
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
|
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
|
||||||
@@ -855,7 +877,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case WM_DRAWITEM:
|
case WM_DRAWITEM:
|
||||||
if((wp>=10 && wp<=13) || (wp>=18 && wp<=26) || wp==34 || wp==35) { drawTool(*reinterpret_cast<DRAWITEMSTRUCT *>(lp),*c); return TRUE; }
|
if((wp>=10 && wp<=13) || (wp>=18 && wp<=27) || wp==34 || wp==35) { drawTool(*reinterpret_cast<DRAWITEMSTRUCT *>(lp),*c); return TRUE; }
|
||||||
break;
|
break;
|
||||||
case WM_CTLCOLORSTATIC:
|
case WM_CTLCOLORSTATIC:
|
||||||
SetBkColor((HDC)wp,RGB(255,255,255)); SetTextColor((HDC)wp,RGB(51,65,85));
|
SetBkColor((HDC)wp,RGB(255,255,255)); SetTextColor((HDC)wp,RGB(51,65,85));
|
||||||
@@ -882,7 +904,8 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
auto draft=c->marks[c->selected];
|
auto draft=c->marks[c->selected];
|
||||||
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=textStyleDefaults(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;
|
||||||
@@ -936,7 +959,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)
|
||||||
@@ -945,8 +968,9 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if(changed) { c->copied=false; c->selected=-1; c->selectedLabel=false; c->wheelDelta=0; c->wheelTarget=-1; }
|
if(changed) { c->copied=false; c->selected=-1; c->selectedLabel=false; c->wheelDelta=0; c->wheelTarget=-1; }
|
||||||
c->syncHistory();
|
c->syncHistory();
|
||||||
}
|
}
|
||||||
if (LOWORD(wp)==10 || LOWORD(wp)==11 || LOWORD(wp)==19 || (LOWORD(wp)>=21 && LOWORD(wp)<=26))
|
if (LOWORD(wp)==10 || LOWORD(wp)==11 || LOWORD(wp)==19 || (LOWORD(wp)>=21 && LOWORD(wp)<=27))
|
||||||
{
|
{
|
||||||
|
c->pointer=LOWORD(wp)==27;
|
||||||
c->arrow = LOWORD(wp)==11 || (LOWORD(wp)>=22 && LOWORD(wp)<=25);
|
c->arrow = LOWORD(wp)==11 || (LOWORD(wp)>=22 && LOWORD(wp)<=25);
|
||||||
c->straight=LOWORD(wp)==22;
|
c->straight=LOWORD(wp)==22;
|
||||||
c->textTool=LOWORD(wp)==26;
|
c->textTool=LOWORD(wp)==26;
|
||||||
@@ -960,7 +984,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
InvalidateRect(GetDlgItem(w,11),nullptr,FALSE);
|
InvalidateRect(GetDlgItem(w,11),nullptr,FALSE);
|
||||||
InvalidateRect(GetDlgItem(w,19),nullptr,FALSE);
|
InvalidateRect(GetDlgItem(w,19),nullptr,FALSE);
|
||||||
InvalidateRect(GetDlgItem(w,21),nullptr,FALSE);
|
InvalidateRect(GetDlgItem(w,21),nullptr,FALSE);
|
||||||
for(int id=22;id<=26;++id) InvalidateRect(GetDlgItem(w,id),nullptr,FALSE);
|
for(int id=22;id<=27;++id) InvalidateRect(GetDlgItem(w,id),nullptr,FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetFocus(w); InvalidateRect(w,nullptr,FALSE); return 0;
|
SetFocus(w); InvalidateRect(w,nullptr,FALSE); return 0;
|
||||||
@@ -996,6 +1020,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
InvalidateRect(w,nullptr,FALSE);
|
InvalidateRect(w,nullptr,FALSE);
|
||||||
}
|
}
|
||||||
if(!(GetKeyState(VK_CONTROL)&0x8000)) {
|
if(!(GetKeyState(VK_CONTROL)&0x8000)) {
|
||||||
|
if(wp=='V') SendMessageW(w,WM_COMMAND,27,0);
|
||||||
if(wp=='T') SendMessageW(w,WM_COMMAND,26,0);
|
if(wp=='T') SendMessageW(w,WM_COMMAND,26,0);
|
||||||
if(wp=='L') SendMessageW(w,WM_COMMAND,22,0);
|
if(wp=='L') SendMessageW(w,WM_COMMAND,22,0);
|
||||||
if(wp=='R') SendMessageW(w,WM_COMMAND,23,0);
|
if(wp=='R') SendMessageW(w,WM_COMMAND,23,0);
|
||||||
@@ -1055,6 +1080,7 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
InvalidateRect(w,nullptr,FALSE); return 0;
|
InvalidateRect(w,nullptr,FALSE); return 0;
|
||||||
}
|
}
|
||||||
c->selected=-1; c->selectedLabel=false;
|
c->selected=-1; c->selectedLabel=false;
|
||||||
|
if(c->pointer) { InvalidateRect(w,nullptr,FALSE); updateStatus(*c); return 0; }
|
||||||
}
|
}
|
||||||
if(c->editing && c->textTool) {
|
if(c->editing && c->textTool) {
|
||||||
Mark draft=c->textDefaults; draft.text.clear(); draft.from=draft.to=p; draft.color=c->currentColor;
|
Mark draft=c->textDefaults; draft.text.clear(); draft.from=draft.to=p; draft.color=c->currentColor;
|
||||||
@@ -1126,7 +1152,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);
|
||||||
@@ -1353,27 +1380,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 &m:marks) {
|
||||||
|
if(!m.id || !used.insert(m.id).second) { m.id=next++; used.insert(m.id); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
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)
|
if(it->number && std::hypot(double(drop.x-it->from.x),double(drop.y-it->from.y))<=it->radius+tolerance) {
|
||||||
{
|
number=it->number; return;
|
||||||
link=it->number;
|
}
|
||||||
break;
|
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;
|
||||||
@@ -1383,9 +1490,21 @@ 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;
|
||||||
|
state[index]=1;
|
||||||
|
auto &m=marks[index];
|
||||||
|
if(m.isLine()) {
|
||||||
|
auto attach=[&](ScreenshotLink &link,POINT &point) {
|
||||||
|
if(!link.target) return;
|
||||||
|
for(size_t i=0;i<marks.size();++i) if(marks[i].id==link.target) {
|
||||||
|
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);
|
auto from=badge(m.fromNumber),to=badge(m.toNumber);
|
||||||
if(!from) m.fromNumber=0;
|
if(!from) m.fromNumber=0;
|
||||||
if(!to) m.toNumber=0;
|
if(!to) m.toNumber=0;
|
||||||
@@ -1394,6 +1513,9 @@ void resolveScreenshotLinks(std::vector<ScreenshotMark> &marks)
|
|||||||
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; }
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
@@ -1461,6 +1583,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);
|
||||||
@@ -1496,6 +1619,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()) {
|
||||||
@@ -1515,6 +1639,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));
|
||||||
|
|||||||
+7
-1
@@ -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);
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.3";
|
inline constexpr wchar_t AppVersion[] = L"0.6.6";
|
||||||
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;
|
||||||
struct UpdateResult
|
struct UpdateResult
|
||||||
|
|||||||
@@ -179,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);
|
||||||
|
|||||||
+120
-2
@@ -36,11 +36,11 @@ int main()
|
|||||||
RECT client{}; GetClientRect(window,&client);
|
RECT client{}; GetClientRect(window,&client);
|
||||||
require(c.view.bottom<=client.bottom-c.statusHeight,"image does not overlap status bar");
|
require(c.view.bottom<=client.bottom-c.statusHeight,"image does not overlap status bar");
|
||||||
ks::updateStatus(c);
|
ks::updateStatus(c);
|
||||||
require(c.statusText.find(L"Freehand")!=std::wstring::npos,"default tool hint is shown");
|
require(c.statusText.find(L"Pointer")!=std::wstring::npos,"default tool hint is shown");
|
||||||
SendMessageW(GetDlgItem(window,13),WM_MOUSEMOVE,0,0);
|
SendMessageW(GetDlgItem(window,13),WM_MOUSEMOVE,0,0);
|
||||||
require(c.statusText.find(L"Ctrl+C")!=std::wstring::npos,"button hover shows action shortcut");
|
require(c.statusText.find(L"Ctrl+C")!=std::wstring::npos,"button hover shows action shortcut");
|
||||||
SendMessageW(GetDlgItem(window,13),WM_MOUSELEAVE,0,0);
|
SendMessageW(GetDlgItem(window,13),WM_MOUSELEAVE,0,0);
|
||||||
require(c.statusText.find(L"Freehand")!=std::wstring::npos,"button leave restores contextual hint");
|
require(c.statusText.find(L"Pointer")!=std::wstring::npos,"button leave restores contextual hint");
|
||||||
for(int id:ks::ToolIds) require(GetDlgItem(window,id)!=nullptr,"all toolbar buttons created");
|
for(int id:ks::ToolIds) require(GetDlgItem(window,id)!=nullptr,"all toolbar buttons created");
|
||||||
require(GetDlgItem(window,34)!=nullptr && c.toolIcons[0][14]!=nullptr,"font icon created and loaded");
|
require(GetDlgItem(window,34)!=nullptr && c.toolIcons[0][14]!=nullptr,"font icon created and loaded");
|
||||||
ks::Mark text{{20,20},{20,20},0}; text.fontSize=18;
|
ks::Mark text{{20,20},{20,20},0}; text.fontSize=18;
|
||||||
@@ -94,6 +94,124 @@ 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");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
};
|
||||||
|
SendMessageW(window,WM_COMMAND,27,0); c.marks.clear();
|
||||||
|
mouse(WM_LBUTTONDOWN,{40,160}); mouse(WM_MOUSEMOVE,{100,190}); mouse(WM_LBUTTONUP,{100,190});
|
||||||
|
require(c.marks.empty() && !c.dragging && c.selected==-1,"pointer drag on empty canvas never adds an object");
|
||||||
|
for(int tool : {11,22}) for(int target : {0,1,2}) {
|
||||||
|
if(target==0) c.marks={{{55,160},{55,160},1},{{255,160},{255,160},2}};
|
||||||
|
else {
|
||||||
|
c.marks={{{30,140},{80,190},0},{{230,140},{280,190},0}};
|
||||||
|
if(target==1) for(auto &mark:c.marks) mark.shape=ks::ScreenshotShape::Rectangle;
|
||||||
|
else for(auto &mark:c.marks) mark.from.y=mark.to.y=160;
|
||||||
|
}
|
||||||
|
SendMessageW(window,WM_COMMAND,tool,0);
|
||||||
|
BYTE savedKeys[256]{},drawingKeys[256]{};
|
||||||
|
require(GetKeyboardState(savedKeys)!=FALSE,"read thread keyboard state");
|
||||||
|
std::copy(savedKeys,savedKeys+256,drawingKeys); drawingKeys[VK_CONTROL]=0x80;
|
||||||
|
require(SetKeyboardState(drawingKeys)!=FALSE,"set thread-local Ctrl for drawing test");
|
||||||
|
mouse(WM_LBUTTONDOWN,{55,160}); mouse(WM_MOUSEMOVE,{255,160}); mouse(WM_LBUTTONUP,{255,160});
|
||||||
|
SetKeyboardState(savedKeys);
|
||||||
|
require(c.marks.size()==3,"drawing over existing objects creates a connector with Ctrl");
|
||||||
|
auto &connector=c.marks[2];
|
||||||
|
require(target==0?(connector.fromNumber==1 && connector.toNumber==2):
|
||||||
|
(connector.fromLink.target==c.marks[0].id && connector.toLink.target==c.marks[1].id),
|
||||||
|
"new arrow or line automatically attaches both endpoints to targets");
|
||||||
|
require(connector.ends==(tool==11?c.arrowEnds:ks::LineEnds::None),"arrow and straight-line tools retain their end styles");
|
||||||
|
auto before=connector.from;
|
||||||
|
SendMessageW(window,WM_KEYDOWN,'V',0);
|
||||||
|
require(c.pointer && !c.arrow && !c.freehand,"V selects pointer mode");
|
||||||
|
POINT body=target==0?POINT{55,160}:target==1?POINT{30,175}:POINT{30,160};
|
||||||
|
mouse(WM_LBUTTONDOWN,body); mouse(WM_MOUSEMOVE,{body.x,body.y-20}); mouse(WM_LBUTTONUP,{body.x,body.y-20});
|
||||||
|
require(c.selected==0 && c.marks.size()==3 && c.marks[2].from.y<before.y,
|
||||||
|
"pointer moves the target and the new connector follows without adding objects");
|
||||||
|
mouse(WM_LBUTTONDOWN,{150,220}); mouse(WM_LBUTTONUP,{150,220});
|
||||||
|
require(c.selected==-1 && c.marks.size()==3,"pointer click on empty space only clears selection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(bool manualAngle : {false,true}) {
|
||||||
|
c.marks={{{40,140},{180,60},0}}; c.selected=0;
|
||||||
|
c.marks[0].fontSize=18; c.marks[0].fontWeight=FW_BOLD;
|
||||||
|
c.marks[0].rotation=31; c.marks[0].labelRotationManual=manualAngle;
|
||||||
|
SendMessageW(window,WM_COMMAND,35,0);
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Sloped caption");
|
||||||
|
ks::finishInlineText(window,c,true);
|
||||||
|
auto captionAngle=ks::screenshotArrowLabel(c.marks[0]).rotation;
|
||||||
|
require(captionAngle!=0,"caption retains its automatic or manual angle");
|
||||||
|
SendMessageW(window,WM_COMMAND,26,0);
|
||||||
|
LONG x=c.view.left+MulDiv(220,c.view.right-c.view.left,c.image->width);
|
||||||
|
LONG y=c.view.top+MulDiv(190,c.view.bottom-c.view.top,c.image->height);
|
||||||
|
SendMessageW(window,WM_LBUTTONDOWN,MK_LBUTTON,MAKELPARAM(x,y));
|
||||||
|
require(c.inlineText && !c.editingLabel,"standalone text starts independently of caption");
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Independent text");
|
||||||
|
ks::finishInlineText(window,c,true);
|
||||||
|
require(c.marks.size()==2 && c.marks[1].isText() && c.marks[1].rotation==0,
|
||||||
|
"new standalone text does not inherit caption rotation");
|
||||||
|
require(c.marks[1].fontSize==18 && c.marks[1].fontWeight==FW_BOLD,
|
||||||
|
"new text still inherits font styling");
|
||||||
|
require(ks::screenshotArrowLabel(c.marks[0]).rotation==captionAngle,"new text does not change caption angle");
|
||||||
|
c.marks[1].rotation=22; c.selected=1;
|
||||||
|
SendMessageW(window,WM_KEYDOWN,VK_F2,0);
|
||||||
|
SendMessageW(c.inlineText,EM_REPLACESEL,TRUE,(LPARAM)L"Edited standalone text");
|
||||||
|
ks::finishInlineText(window,c,true);
|
||||||
|
require(c.marks[1].rotation==22,"editing existing rotated text preserves its own angle");
|
||||||
|
SendMessageW(window,WM_COMMAND,27,0);
|
||||||
|
}
|
||||||
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});
|
||||||
|
|||||||
Reference in New Issue
Block a user