fix: align text annotations in captures

This commit is contained in:
2026-07-08 14:46:57 +08:00
parent f90612976f
commit 7b290d9300
7 changed files with 533 additions and 130 deletions
+193 -17
View File
@@ -115,6 +115,12 @@ static id nativeOverlayKeyMonitor = nil;
@property(strong) NSView *actionToolbarBg;
@property(strong) NSTextField *textEditor;
@property NSPoint textEditorLocalPoint;
@property NSInteger textEditorAnnotationIndex;
@property(strong) NSColor *textEditorColor;
@property NSInteger selectedTextAnnotationIndex;
@property BOOL draggingTextAnnotation;
@property NSPoint textDragStartLocalPoint;
@property NSPoint textDragOriginalLocalPoint;
@property(strong) NSTextField *sizeLabel;
@property(strong) NSTextField *hintLabel;
- (void)syncControls;
@@ -127,6 +133,10 @@ static id nativeOverlayKeyMonitor = nil;
- (void)summarizeSelection;
- (void)cancelSelection;
- (void)beginTextAnnotationAtPoint:(NSPoint)localPoint;
- (void)beginEditingTextAnnotationAtIndex:(NSInteger)index;
- (NSInteger)textAnnotationIndexAtPoint:(NSPoint)p;
- (NSRect)textAnnotationRectForItem:(NSDictionary *)item;
- (NSPoint)clampedTextLocalPoint:(NSPoint)localPoint text:(NSString *)text fontSize:(CGFloat)fontSize;
- (BOOL)isEditingText;
- (void)commitTextEditor;
- (void)cancelTextEditor;
@@ -145,6 +155,8 @@ static id nativeOverlayKeyMonitor = nil;
_activeTool = nil;
_activeColor = [NSColor colorWithCalibratedRed:239.0/255.0 green:68.0/255.0 blue:68.0/255.0 alpha:1.0];
_annotations = [NSMutableArray array];
_textEditorAnnotationIndex = -1;
_selectedTextAnnotationIndex = -1;
// Use SnipHoverButton for the 5 action buttons so we get hover-color
// animation. Annotation buttons stay as plain NSButton because they
@@ -364,12 +376,73 @@ static id nativeOverlayKeyMonitor = nil;
}
}
- (CGFloat)fontSizeForTextItem:(NSDictionary *)item {
NSNumber *fontSize = item[@"fontSize"];
if (fontSize != nil && [fontSize doubleValue] > 0) {
return [fontSize doubleValue];
}
return 20.0;
}
- (NSDictionary *)textAttributesWithFontSize:(CGFloat)fontSize color:(NSColor *)color {
return @{
NSFontAttributeName: [NSFont systemFontOfSize:fontSize weight:NSFontWeightSemibold],
NSForegroundColorAttributeName: color ?: [NSColor whiteColor]
};
}
- (NSSize)textSizeForString:(NSString *)text fontSize:(CGFloat)fontSize {
NSString *measured = text.length > 0 ? text : @" ";
NSDictionary *attrs = [self textAttributesWithFontSize:fontSize color:[NSColor whiteColor]];
NSRect bounds = [measured boundingRectWithSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:attrs];
return NSMakeSize(ceil(MAX(1.0, bounds.size.width)), ceil(MAX(fontSize * 1.2, bounds.size.height)));
}
- (NSRect)textAnnotationRectForItem:(NSDictionary *)item {
if (![item[@"tool"] isEqualToString:@"text"]) {
return NSZeroRect;
}
NSArray *points = item[@"points"];
NSString *text = item[@"text"];
if (points.count == 0 || text.length == 0) {
return NSZeroRect;
}
NSPoint p = [points[0] pointValue];
CGFloat fontSize = [self fontSizeForTextItem:item];
NSSize size = [self textSizeForString:text fontSize:fontSize];
return NSMakeRect(_selection.origin.x + p.x, _selection.origin.y + p.y, size.width, size.height);
}
- (NSPoint)clampedTextLocalPoint:(NSPoint)localPoint text:(NSString *)text fontSize:(CGFloat)fontSize {
NSSize size = [self textSizeForString:text fontSize:fontSize];
CGFloat maxX = MAX(0, _selection.size.width - size.width);
CGFloat maxY = MAX(0, _selection.size.height - size.height);
return NSMakePoint(MAX(0, MIN(localPoint.x, maxX)), MAX(0, MIN(localPoint.y, maxY)));
}
- (NSInteger)textAnnotationIndexAtPoint:(NSPoint)p {
for (NSInteger i = (NSInteger)_annotations.count - 1; i >= 0; i--) {
NSDictionary *item = _annotations[(NSUInteger)i];
if (![item[@"tool"] isEqualToString:@"text"]) {
continue;
}
NSRect hitRect = NSInsetRect([self textAnnotationRectForItem:item], -6, -6);
if (!NSIsEmptyRect(hitRect) && NSPointInRect(p, hitRect)) {
return i;
}
}
return -1;
}
- (void)drawAnnotations {
NSMutableArray *items = [NSMutableArray arrayWithArray:_annotations];
if (_draftAnnotation != nil) {
[items addObject:_draftAnnotation];
}
for (NSDictionary *item in items) {
for (NSUInteger index = 0; index < items.count; index++) {
NSDictionary *item = items[index];
NSString *tool = item[@"tool"];
NSColor *color = item[@"nsColor"];
NSArray *points = item[@"points"];
@@ -382,11 +455,18 @@ static id nativeOverlayKeyMonitor = nil;
continue;
}
NSPoint p = [points[0] pointValue];
NSDictionary *attrs = @{
NSFontAttributeName: [NSFont systemFontOfSize:20 weight:NSFontWeightSemibold],
NSForegroundColorAttributeName: color ?: [NSColor whiteColor]
};
CGFloat fontSize = [self fontSizeForTextItem:item];
NSDictionary *attrs = [self textAttributesWithFontSize:fontSize color:color];
[text drawAtPoint:NSMakePoint(_selection.origin.x + p.x, _selection.origin.y + p.y) withAttributes:attrs];
if ((NSInteger)index == _selectedTextAnnotationIndex && index < _annotations.count) {
NSRect rect = NSInsetRect([self textAnnotationRectForItem:item], -4, -3);
NSBezierPath *selectionPath = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:4 yRadius:4];
[selectionPath setLineWidth:1.5];
CGFloat dash[] = {4.0, 3.0};
[selectionPath setLineDash:dash count:2 phase:0];
[[NSColor colorWithCalibratedRed:59.0/255.0 green:130.0/255.0 blue:246.0/255.0 alpha:0.95] setStroke];
[selectionPath stroke];
}
continue;
}
[color setStroke];
@@ -501,6 +581,7 @@ static id nativeOverlayKeyMonitor = nil;
if (_textEditor != nil) {
[self commitTextEditor];
}
_draggingTextAnnotation = NO;
NSString *handle = [self resizeHandleAtPoint:p];
if (handle != nil) {
_resizing = YES;
@@ -512,6 +593,31 @@ static id nativeOverlayKeyMonitor = nil;
[self syncControls];
return;
}
NSInteger textIndex = _hasSelection ? [self textAnnotationIndexAtPoint:p] : -1;
if (textIndex >= 0) {
_selectedTextAnnotationIndex = textIndex;
_creating = NO;
_moving = NO;
_resizing = NO;
_annotating = NO;
if ([event clickCount] >= 2) {
[self beginEditingTextAnnotationAtIndex:textIndex];
[self syncControls];
[self setNeedsDisplay:YES];
return;
}
NSDictionary *item = _annotations[(NSUInteger)textIndex];
NSArray *points = item[@"points"];
if (points.count > 0) {
_draggingTextAnnotation = YES;
_textDragStartLocalPoint = [self localPoint:p];
_textDragOriginalLocalPoint = [points[0] pointValue];
}
[self syncControls];
[self setNeedsDisplay:YES];
return;
}
_selectedTextAnnotationIndex = -1;
if (_hasSelection && NSPointInRect(p, _selection) && [_activeTool isEqualToString:@"text"]) {
_creating = NO;
_moving = NO;
@@ -553,6 +659,7 @@ static id nativeOverlayKeyMonitor = nil;
_selection = NSMakeRect(p.x, p.y, 0, 0);
[_annotations removeAllObjects];
_draftAnnotation = nil;
_selectedTextAnnotationIndex = -1;
}
[self syncControls];
}
@@ -583,6 +690,17 @@ static id nativeOverlayKeyMonitor = nil;
top = MAX(0, MIN(top, self.bounds.size.height));
bottom = MAX(0, MIN(bottom, self.bounds.size.height));
_selection = NSMakeRect(MIN(left, right), MIN(top, bottom), fabs(right - left), fabs(bottom - top));
} else if (_draggingTextAnnotation && _selectedTextAnnotationIndex >= 0 && _selectedTextAnnotationIndex < (NSInteger)_annotations.count) {
NSMutableDictionary *item = (NSMutableDictionary *)_annotations[(NSUInteger)_selectedTextAnnotationIndex];
NSMutableArray *points = (NSMutableArray *)item[@"points"];
if (points.count > 0) {
NSPoint local = [self localPoint:p];
NSPoint delta = NSMakePoint(local.x - _textDragStartLocalPoint.x, local.y - _textDragStartLocalPoint.y);
NSPoint next = NSMakePoint(_textDragOriginalLocalPoint.x + delta.x, _textDragOriginalLocalPoint.y + delta.y);
NSString *text = item[@"text"] ?: @"";
CGFloat fontSize = [self fontSizeForTextItem:item];
points[0] = [NSValue valueWithPoint:[self clampedTextLocalPoint:next text:text fontSize:fontSize]];
}
} else if (_annotating && _draftAnnotation != nil) {
NSMutableArray *points = _draftAnnotation[@"points"];
NSPoint local = [self localPoint:p];
@@ -604,6 +722,7 @@ static id nativeOverlayKeyMonitor = nil;
_creating = NO;
_moving = NO;
_resizing = NO;
_draggingTextAnnotation = NO;
if (_annotating && _draftAnnotation != nil) {
[_annotations addObject:_draftAnnotation];
_draftAnnotation = nil;
@@ -731,32 +850,68 @@ static id nativeOverlayKeyMonitor = nil;
[self cancelTextEditor];
if (_annotations.count > 0) {
[_annotations removeLastObject];
if (_selectedTextAnnotationIndex >= (NSInteger)_annotations.count) {
_selectedTextAnnotationIndex = -1;
}
[self syncControls];
[self setNeedsDisplay:YES];
}
}
- (void)beginTextAnnotationAtPoint:(NSPoint)localPoint {
[self beginTextEditorAtLocalPoint:localPoint text:@"" color:_activeColor annotationIndex:-1];
}
- (void)beginEditingTextAnnotationAtIndex:(NSInteger)index {
if (index < 0 || index >= (NSInteger)_annotations.count) {
return;
}
NSDictionary *item = _annotations[(NSUInteger)index];
if (![item[@"tool"] isEqualToString:@"text"]) {
return;
}
NSArray *points = item[@"points"];
if (points.count == 0) {
return;
}
NSColor *color = item[@"nsColor"] ?: _activeColor;
[self beginTextEditorAtLocalPoint:[points[0] pointValue]
text:item[@"text"] ?: @""
color:color
annotationIndex:index];
}
- (void)beginTextEditorAtLocalPoint:(NSPoint)localPoint text:(NSString *)text color:(NSColor *)color annotationIndex:(NSInteger)index {
[self cancelTextEditor];
_textEditorLocalPoint = localPoint;
NSRect frame = NSMakeRect(_selection.origin.x + localPoint.x, _selection.origin.y + localPoint.y, 180, 30);
CGFloat fontSize = 20.0;
NSPoint clampedLocal = [self clampedTextLocalPoint:localPoint text:text fontSize:fontSize];
_textEditorLocalPoint = clampedLocal;
_textEditorAnnotationIndex = index;
_textEditorColor = color ?: _activeColor;
NSSize textSize = [self textSizeForString:text fontSize:fontSize];
CGFloat editorWidth = MAX(180, MIN(360, textSize.width + 24));
NSRect frame = NSMakeRect(_selection.origin.x + clampedLocal.x, _selection.origin.y + clampedLocal.y, editorWidth, 30);
CGFloat maxX = self.bounds.size.width - frame.size.width - 8;
CGFloat maxY = self.bounds.size.height - frame.size.height - 8;
frame.origin.x = MAX(8, MIN(frame.origin.x, maxX));
frame.origin.y = MAX(8, MIN(frame.origin.y, maxY));
_textEditorLocalPoint = [self localPoint:frame.origin];
_textEditor = [[NSTextField alloc] initWithFrame:frame];
[_textEditor setBezeled:YES];
[_textEditor setBordered:YES];
[_textEditor setDrawsBackground:YES];
[_textEditor setBackgroundColor:[NSColor colorWithCalibratedWhite:0.08 alpha:0.78]];
[_textEditor setTextColor:_activeColor];
[_textEditor setFont:[NSFont systemFontOfSize:20 weight:NSFontWeightSemibold]];
[_textEditor setTextColor:_textEditorColor];
[_textEditor setFont:[NSFont systemFontOfSize:fontSize weight:NSFontWeightSemibold]];
[_textEditor setStringValue:text ?: @""];
[_textEditor setFocusRingType:NSFocusRingTypeNone];
[_textEditor setTarget:self];
[_textEditor setAction:@selector(commitTextEditorFromSender:)];
[self addSubview:_textEditor];
[[self window] makeFirstResponder:_textEditor];
[_textEditor selectText:nil];
}
- (BOOL)isEditingText {
@@ -772,17 +927,35 @@ static id nativeOverlayKeyMonitor = nil;
return;
}
NSString *text = [[_textEditor stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (text.length > 0) {
NSColor *textColor = _textEditorColor ?: _activeColor;
if (_textEditorAnnotationIndex >= 0 && _textEditorAnnotationIndex < (NSInteger)_annotations.count) {
if (text.length > 0) {
NSMutableDictionary *item = (NSMutableDictionary *)_annotations[(NSUInteger)_textEditorAnnotationIndex];
item[@"color"] = [self hexForColor:textColor];
item[@"nsColor"] = textColor;
item[@"points"] = [NSMutableArray arrayWithObject:[NSValue valueWithPoint:_textEditorLocalPoint]];
item[@"text"] = text;
item[@"fontSize"] = @20.0;
_selectedTextAnnotationIndex = _textEditorAnnotationIndex;
} else {
[_annotations removeObjectAtIndex:(NSUInteger)_textEditorAnnotationIndex];
_selectedTextAnnotationIndex = -1;
}
} else if (text.length > 0) {
[_annotations addObject:[@{
@"tool": @"text",
@"color": [self hexForColor:_activeColor],
@"nsColor": _activeColor,
@"color": [self hexForColor:textColor],
@"nsColor": textColor,
@"points": [NSMutableArray arrayWithObject:[NSValue valueWithPoint:_textEditorLocalPoint]],
@"text": text
@"text": text,
@"fontSize": @20.0
} mutableCopy]];
_selectedTextAnnotationIndex = (NSInteger)_annotations.count - 1;
}
[_textEditor removeFromSuperview];
_textEditor = nil;
_textEditorAnnotationIndex = -1;
_textEditorColor = nil;
[[self window] makeFirstResponder:self];
[self syncControls];
[self setNeedsDisplay:YES];
@@ -794,6 +967,8 @@ static id nativeOverlayKeyMonitor = nil;
}
[_textEditor removeFromSuperview];
_textEditor = nil;
_textEditorAnnotationIndex = -1;
_textEditorColor = nil;
[[self window] makeFirstResponder:self];
[self setNeedsDisplay:YES];
}
@@ -848,6 +1023,7 @@ static id nativeOverlayKeyMonitor = nil;
NSString *text = item[@"text"];
if (text.length > 0) {
entry[@"text"] = text;
entry[@"fontSize"] = item[@"fontSize"] ?: @20.0;
}
[payload addObject:entry];
}
@@ -872,8 +1048,8 @@ static id nativeOverlayKeyMonitor = nil;
return;
}
NSRect r = [self globalRectForSelection:_selection];
[self closeOverlayWindow];
NSString *json = [self annotationsJSON];
[self closeOverlayWindow];
nativeOverlayConfirm((int)llround(r.origin.x), (int)llround(r.origin.y), (int)llround(r.size.width), (int)llround(r.size.height), [json UTF8String]);
}
@@ -882,8 +1058,8 @@ static id nativeOverlayKeyMonitor = nil;
return;
}
NSRect r = [self globalRectForSelection:_selection];
[self closeOverlayWindow];
NSString *json = [self annotationsJSON];
[self closeOverlayWindow];
nativeOverlayCopy((int)llround(r.origin.x), (int)llround(r.origin.y), (int)llround(r.size.width), (int)llround(r.size.height), [json UTF8String]);
}
@@ -925,8 +1101,8 @@ static id nativeOverlayKeyMonitor = nil;
return;
}
NSRect r = [self globalRectForSelection:_selection];
[self closeOverlayWindow];
NSString *json = [self annotationsJSON];
[self closeOverlayWindow];
nativeOverlaySaveRemote((int)llround(r.origin.x), (int)llround(r.origin.y), (int)llround(r.size.width), (int)llround(r.size.height), [json UTF8String]);
}
@@ -937,8 +1113,8 @@ static id nativeOverlayKeyMonitor = nil;
return;
}
NSRect r = [self globalRectForSelection:_selection];
[self closeOverlayWindow];
NSString *json = [self annotationsJSON];
[self closeOverlayWindow];
nativeOverlaySummarize((int)llround(r.origin.x), (int)llround(r.origin.y), (int)llround(r.size.width), (int)llround(r.size.height), [json UTF8String]);
}
@end