improved: tabstop handles

This commit is contained in:
daboe01
2026-05-29 11:41:39 +02:00
parent 41c422ed03
commit c704880e13
3 changed files with 227 additions and 15 deletions
+124 -10
View File
@@ -25,6 +25,8 @@
@import "CPTextField.j"
@import "CPColor.j"
@import "CPFont.j"
@import "CPMenu.j"
@import "CPMenuItem.j"
// Orientations matching AppKit standards
// typedef enum CPRulerOrientation
@@ -36,11 +38,11 @@ CPRulerOrientationVertical = 1
@class CPRulerView;
// MARK: - CPRulerMarker (Interactive High-Res DOM Handle)
// MARK: - CPRulerMarker (Interactive Handles with Dynamic Alignment Icons)
@implementation CPRulerMarker : CPView
{
CPRulerView _rulerView @accessors(readonly, property=rulerView);
CPRulerView _rulerView @accessors(property=rulerView);
float _imageValue @accessors(property=imageValue);
id _representedObject @accessors(property=representedObject);
CPTextField _label;
@@ -48,24 +50,140 @@ CPRulerOrientationVertical = 1
- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation imageValue:(float)anImageValue representedObject:(id)anObject
{
// Render a crisp, resizable 12x12 container for the Unicode indicator
if (self = [super initWithFrame:CGRectMake(0, 0, 12, 12)])
{
_rulerView = aRulerView;
_imageValue = anImageValue;
_representedObject = anObject;
// Beautiful, razor-sharp upward-pointing triangle for high-res screens
_label = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
[_label setStringValue:@"▲"];
[_label setFont:[CPFont systemFontOfSize:10.0]];
[_label setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
[_label setAlignment:CPCenterTextAlignment];
[self addSubview:_label];
[self updateMarkerIcon];
}
return self;
}
- (CPTextField)label
{
return _label;
}
- (void)setRepresentedObject:(id)anObject
{
_representedObject = anObject;
[self updateMarkerIcon];
}
// Dynamically sets the Unicode triangle direction based on the alignment or indent type
- (void)updateMarkerIcon
{
if ([_representedObject isKindOfClass:[CPTextTab class]])
{
var align = [_representedObject alignment];
if (align === CPLeftTextAlignment)
[_label setStringValue:@"▶"]; // Left-aligned points Right
else if (align === CPCenterTextAlignment)
[_label setStringValue:@"▼"]; // Center-aligned points Down
else if (align === CPRightTextAlignment)
[_label setStringValue:@"◀"]; // Right-aligned points Left
}
else
{
[_label setStringValue:@"▲"]; // Indent markers point Up
}
}
#pragma mark -
#pragma mark Context Menu Support
- (CPMenu)menuForEvent:(CPEvent)anEvent
{
var menu = [[CPMenu alloc] initWithTitle:@"Marker Context Menu"];
// If the marker represents a standard tab stop, allow changing its type
if ([_representedObject isKindOfClass:[CPTextTab class]])
{
var itemLeft = [menu addItemWithTitle:@"Left Tab Stop" action:@selector(changeTypeToLeft:) keyEquivalent:@""],
itemCenter = [menu addItemWithTitle:@"Center Tab Stop" action:@selector(changeTypeToCenter:) keyEquivalent:@""],
itemRight = [menu addItemWithTitle:@"Right Tab Stop" action:@selector(changeTypeToRight:) keyEquivalent:@""];
[itemLeft setTarget:self];
[itemCenter setTarget:self];
[itemRight setTarget:self];
var align = [_representedObject alignment];
if (align === CPLeftTextAlignment) [itemLeft setState:CPOnState];
else if (align === CPCenterTextAlignment) [itemCenter setState:CPOnState];
else if (align === CPRightTextAlignment) [itemRight setState:CPOnState];
[menu addItem:[CPMenuItem separatorItem]];
}
// Determine the context-specific delete title
var deleteTitle = @"Delete Tab Stop";
if ([_representedObject isKindOfClass:[CPString class]])
{
if (_representedObject === @"CPFirstLineIndent")
deleteTitle = @"Delete 1st line indentation marker";
else if (_representedObject === @"CPHeadIndent")
deleteTitle = @"Delete head indentation marker";
else if (_representedObject === @"CPTailIndent")
deleteTitle = @"Delete tail indentation marker";
}
var itemDelete = [menu addItemWithTitle:deleteTitle action:@selector(deleteMarker:) keyEquivalent:@""];
[itemDelete setTarget:self];
return menu;
}
- (void)changeTypeToLeft:(id)sender
{
[self _changeAlignment:CPLeftTextAlignment];
}
- (void)changeTypeToCenter:(id)sender
{
[self _changeAlignment:CPCenterTextAlignment];
}
- (void)changeTypeToRight:(id)sender
{
[self _changeAlignment:CPRightTextAlignment];
}
- (void)_changeAlignment:(CPTextAlignment)alignment
{
if (![_representedObject isKindOfClass:[CPTextTab class]])
return;
var oldTab = _representedObject;
var newTab = [[CPTextTab alloc] initWithType:alignment location:_imageValue];
// Using setRepresentedObject: automatically updates the marker triangle direction
[self setRepresentedObject:newTab];
var client = [_rulerView clientView];
if (client && [client respondsToSelector:@selector(rulerView:didUpdateMarker:oldTab:)])
{
[client rulerView:_rulerView didUpdateMarker:self oldTab:oldTab];
}
}
- (void)deleteMarker:(id)sender
{
var client = [_rulerView clientView];
if (client && [client respondsToSelector:@selector(rulerView:didRemoveMarker:)])
{
[client rulerView:_rulerView didRemoveMarker:self];
}
[_rulerView removeMarker:self];
}
@end
@@ -215,11 +333,6 @@ CPRulerOrientationVertical = 1
_draggingMarker = newMarker;
_dragStartPoint = localPoint;
_dragStartLocation = rulerLocation;
// NOTIFY CLIENT OF THE NEW MARKER ADDITION
var client = [self clientView];
if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)])
[client rulerView:self didAddMarker:newMarker];
}
}
@@ -293,6 +406,7 @@ CPRulerOrientationVertical = 1
_draggingMarker = nil;
}
#pragma mark -
#pragma mark DOM Layout Builder
+39 -2
View File
@@ -2465,14 +2465,12 @@ Sets the selection to a range of characters in response to user action.
markerLocation:[paragraphStyle firstLineHeadIndent]
imageValue:[paragraphStyle firstLineHeadIndent]
representedObject:@"CPFirstLineIndent"];
[firstLineMarker._label setStringValue:@"▼"]; // downward arrow styling
[markers addObject:firstLineMarker];
var headMarker = [[CPRulerMarker alloc] initWithRulerView:ruler
markerLocation:[paragraphStyle headIndent]
imageValue:[paragraphStyle headIndent]
representedObject:@"CPHeadIndent"];
[headMarker._label setStringValue:@"▼"];
[markers addObject:headMarker];
[ruler setMarkers:markers];
@@ -2638,6 +2636,45 @@ var compareTabStops = function(obj1, obj2, context) {
}
}
- (void)rulerView:(CPRulerView)rulerView didUpdateMarker:(CPRulerMarker)marker oldTab:(id)oldTab
{
var selectedRange = [self selectedRange];
if (selectedRange.length === 0)
selectedRange = [self selectionRangeForProposedRange:CPMakeRange(selectedRange.location, 0) granularity:CPSelectByParagraph];
if (selectedRange.length === 0)
return;
var paragraphStyle = [[self textStorage] attribute:CPParagraphStyleAttributeName atIndex:selectedRange.location effectiveRange:NULL];
if (!paragraphStyle)
paragraphStyle = [CPParagraphStyle defaultParagraphStyle];
var mutableStyle = [paragraphStyle mutableCopy],
newTab = [marker representedObject],
tabs = [[mutableStyle tabStops] mutableCopy];
[tabs removeObject:oldTab];
[tabs addObject:newTab];
// Sort tabs ascending
[tabs sortUsingFunction:compareTabStops context:nil];
[mutableStyle setTabStops:tabs];
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(selectedRange)];
[_layoutManager textStorage:_textStorage
edited:0
range:CPMakeRangeCopy(selectedRange)
changeInLength:0
invalidatedRange:CPMakeRangeCopy(selectedRange)];
// Force layouts and view canvas update
[_layoutManager _validateLayoutAndGlyphs];
[self sizeToFit];
[self setNeedsDisplay:YES];
}
@end
@implementation CPTextView (CPTextViewDelegate)
+64 -3
View File
@@ -6,7 +6,7 @@
* All modifications copyright Daniel Boehringer 2013.
* Extensive code formatting and review by Andrew Hankinson
* Based on original work by
* Emmanuel Maillard on 27/02/2010.
* Created by Emmanuel Maillard on 27/02/2010.
* Copyright Emmanuel Maillard 2010.
*
* This library is free software; you can redistribute it and/or
@@ -268,6 +268,11 @@ var CPSystemTypesetterFactory,
currentParagraphMaximumLineHeight,
currentParagraphLineSpacing;
// Track paragraph indents and margins
var isFirstLineOfLayout = YES,
isFirstLineOfParagraph = YES,
rightMargin = containerSizeWidth;
if (glyphIndex > 0)
lineOrigin = CGPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin);
else if ([_layoutManager extraLineFragmentTextContainer])
@@ -294,6 +299,57 @@ var CPSystemTypesetterFactory,
currentParagraphMaximumLineHeight = [_currentParagraph maximumLineHeight];
currentParagraphLineSpacing = [_currentParagraph lineSpacing];
// Recalculate right margin on paragraph style change
var tailIndent = [_currentParagraph tailIndent];
if (tailIndent > 0.0)
rightMargin = tailIndent;
else if (tailIndent < 0.0)
rightMargin = containerSizeWidth + tailIndent;
else
rightMargin = containerSizeWidth;
// If we are at the start of a line (no characters processed yet),
// we must update lineOrigin.x to use the newly loaded paragraph style!
if (lineRange.length === 0)
{
if (glyphIndex > 0)
{
var prevChar = theString.charCodeAt(glyphIndex - 1);
isFirstLineOfParagraph = (prevChar === 10 || prevChar === 13);
}
else
{
isFirstLineOfParagraph = YES;
}
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
isFirstLineOfLayout = NO;
}
// Calculate the right wrapping margin based on tail indent
var tailIndent = [_currentParagraph tailIndent];
if (tailIndent > 0.0)
rightMargin = tailIndent;
else if (tailIndent < 0.0)
rightMargin = containerSizeWidth + tailIndent;
else
rightMargin = containerSizeWidth;
// Handle the layout's very first line indentation
if (isFirstLineOfLayout)
{
if (glyphIndex > 0)
{
var prevChar = theString.charCodeAt(glyphIndex - 1);
isFirstLineOfParagraph = (prevChar === 10 || prevChar === 13);
}
else
{
isFirstLineOfParagraph = YES;
}
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
isFirstLineOfLayout = NO;
}
if (!currentFont)
currentFont = [_textStorage font] || [CPFont systemFontOfSize:12.0];
@@ -432,7 +488,8 @@ var CPSystemTypesetterFactory,
advancements.push({width: rangeWidth - prevRangeWidth, height: ascent, descent: descent});
prevRangeWidth = _lineWidth = rangeWidth;
if (lineOrigin.x + rangeWidth > containerSizeWidth)
// Wrap lines against the tail indent (rightMargin) instead of container boundaries
if (lineOrigin.x + rangeWidth > rightMargin)
{
if (wrapWidth)
{
@@ -476,7 +533,11 @@ var CPSystemTypesetterFactory,
containerSizeHeight = containerSize.height;
}
lineOrigin.x = 0;
// If this is a soft wrap (isWordWrapped), next line gets headIndent.
// If it was a paragraph return, it gets firstLineHeadIndent.
isFirstLineOfParagraph = !isWordWrapped;
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
numLines++;
isNewline = NO;
_lineFragments = [];