From d180a25f2e25d608806bcf3b6d5a5d96e2f338e8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 16 Jan 2026 17:08:50 +0100 Subject: [PATCH 01/39] new: CPRulerview and friends --- AppKit/CPTextView/CPRulerView.j | 1086 +++++++++++++++++++++++++++++++ 1 file changed, 1086 insertions(+) create mode 100644 AppKit/CPTextView/CPRulerView.j diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j new file mode 100644 index 000000000..008d212bf --- /dev/null +++ b/AppKit/CPTextView/CPRulerView.j @@ -0,0 +1,1086 @@ +/* + + CPRulerView.j + Created by Daniel Boehringer on 08/01/2016 + + Copyright Daniel Boehringer 2016. + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +@import +@import +@import +@import +@import +@import +@import +@import + +@class CPRulerMarker + +@global CPHorizontalRuler +@global CPVerticalRuler + +// Constants for layout +var DEFAULT_RULE_THICKNESS = 16.0, + DEFAULT_MARKER_THICKNESS = 15.0, + HASH_MARK_THICKNESS_FACTOR = 0.6, + HASH_MARK_WIDTH = 1.0, + LABEL_TEXT_PADDING = 2.0; + +var _measurementUnits = nil; + +// ----------------------------------------------------------------------------- +// Helper Class: _CPMeasurementUnit +// ----------------------------------------------------------------------------- + +@implementation _CPMeasurementUnit : CPObject +{ + CPString _name @accessors(property = name); + CPString _abbreviation @accessors(property = abbreviation); + float _pointsPerUnit @accessors(property = pointsPerUnit); + CPArray _stepUpCycle @accessors(property = stepUpCycle); + CPArray _stepDownCycle @accessors(property = stepDownCycle); +} + ++ (void)initialize +{ + if (self !== [_CPMeasurementUnit class]) + return; + + _measurementUnits = []; + + [self registerUnit:[_CPMeasurementUnit inchesMeasurementUnit]]; + [self registerUnit:[_CPMeasurementUnit centimetersMeasurementUnit]]; + [self registerUnit:[_CPMeasurementUnit pointsMeasurementUnit]]; + [self registerUnit:[_CPMeasurementUnit picasMeasurementUnit]]; +} + ++ (_CPMeasurementUnit)measurementUnitWithName:(CPString)name abbreviation:(CPString)abbreviation pointsPerUnit:(float)points stepUpCycle:(CPArray)upCycle stepDownCycle:(CPArray)downCycle +{ + return [[self alloc] initWithName:name abbreviation:abbreviation pointsPerUnit:points stepUpCycle:upCycle stepDownCycle:downCycle]; +} + +- (id)initWithName:(CPString)name abbreviation:(CPString)abbreviation pointsPerUnit:(float)points stepUpCycle:(CPArray)upCycle stepDownCycle:(CPArray)downCycle +{ + self = [super init]; + if (self) + { + _name = name; + _abbreviation = abbreviation; + _pointsPerUnit = points; + _stepUpCycle = upCycle; + _stepDownCycle = downCycle; + } + return self; +} + ++ (_CPMeasurementUnit)inchesMeasurementUnit +{ + return [self measurementUnitWithName:@"Inches" + abbreviation:@"in" + pointsPerUnit:72.0 + stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ] + stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.25], [CPNumber numberWithFloat:0.125] ]]; +} + ++ (_CPMeasurementUnit)centimetersMeasurementUnit +{ + return [self measurementUnitWithName:@"Centimeters" + abbreviation:@"cm" + pointsPerUnit:28.3465 + stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ] + stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.1] ]]; +} + ++ (_CPMeasurementUnit)pointsMeasurementUnit +{ + return [self measurementUnitWithName:@"Points" + abbreviation:@"pt" + pointsPerUnit:1.0 + stepUpCycle:[ [CPNumber numberWithFloat:10.0], [CPNumber numberWithFloat:50.0], [CPNumber numberWithFloat:100.0] ] + stepDownCycle:[ [CPNumber numberWithFloat:0.5] ]]; // Points rarely sub-divide +} + ++ (_CPMeasurementUnit)picasMeasurementUnit +{ + return [self measurementUnitWithName:@"Picas" + abbreviation:@"pc" + pointsPerUnit:12.0 + stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ] + stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.0833] ]]; // 6p and 1pt +} + ++ (CPArray)allMeasurementUnits +{ + return _measurementUnits; +} + ++ (_CPMeasurementUnit)measurementUnitNamed:(CPString)name +{ + var i = 0, count = [_measurementUnits count]; + for (; i < count; ++i) + if ([[[_measurementUnits objectAtIndex:i] name] isEqualToString:name]) + return [_measurementUnits objectAtIndex:i]; + return nil; +} + ++ (void)registerUnit:(_CPMeasurementUnit)unit +{ + if (!_measurementUnits) _measurementUnits = []; + [_measurementUnits addObject:unit]; +} + +@end + +// ----------------------------------------------------------------------------- +// Class: CPRulerView +// ----------------------------------------------------------------------------- + +@implementation CPRulerView : CPView +{ + CPScrollView _scrollView; + CPView _clientView; + CPView _accessoryView; + CPArray _markers; + _CPMeasurementUnit _measurementUnit; + CPMutableArray _rulerlineLocations; + + float _originOffset; + float _ruleThickness; + float _thicknessForMarkers; + float _thicknessForAccessoryView; + + CPRulerOrientation _orientation; + + // Cache attributes + CPDictionary _labelAttributes; +} + +// MARK: - Class Methods + ++ (void)registerUnitWithName:(CPString)name abbreviation:(CPString)abbreviation unitToPointsConversionFactor:(float)conversionFactor stepUpCycle:(CPArray)stepUpCycle stepDownCycle:(CPArray)stepDownCycle +{ + [_CPMeasurementUnit registerUnit:[_CPMeasurementUnit measurementUnitWithName:name abbreviation:abbreviation pointsPerUnit:conversionFactor stepUpCycle:stepUpCycle stepDownCycle:stepDownCycle]]; +} + +// MARK: - Initialization + +- (id)initWithFrame:(CPRect)frame +{ + return [self initWithScrollView:nil orientation:CPHorizontalRuler]; +} + +- (id)initWithScrollView:(CPScrollView)scrollView orientation:(CPRulerOrientation)orientation +{ + var frame = CPMakeRect(0, 0, 1, 1); + + // Determine initial frame based on standard thickness + if (orientation == CPHorizontalRuler) + frame.size.height = DEFAULT_RULE_THICKNESS; + else + frame.size.width = DEFAULT_RULE_THICKNESS; + + self = [super initWithFrame:frame]; + + if (self) + { + _scrollView = scrollView; + _orientation = orientation; + _measurementUnit = [_CPMeasurementUnit measurementUnitNamed:@"Inches"]; + + _ruleThickness = DEFAULT_RULE_THICKNESS; + _thicknessForMarkers = 0.0; // Grows as needed + _thicknessForAccessoryView = 0.0; + _originOffset = 0.0; + + _markers = []; + _rulerlineLocations = []; + + var style = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; + [style setLineBreakMode:CPLineBreakByClipping]; + [style setAlignment:CPLeftTextAlignment]; + _labelAttributes = @{ + CPFontAttributeName: [CPFont systemFontOfSize:9.0], + CPParagraphStyleAttributeName: style, + CPForegroundColorAttributeName: [CPColor blackColor] + }; + } + + return self; +} + +- (id)initWithCoder:(CPCoder)aCoder +{ + self = [super initWithCoder:aCoder]; + if (self) + { + _scrollView = [aCoder decodeObjectForKey:@"CPScrollView"]; + _orientation = [aCoder decodeIntForKey:@"CPOrientation"]; + _markers = [aCoder decodeObjectForKey:@"CPMarkers"]; + if (!_markers) _markers = []; + + // Defaults + _ruleThickness = DEFAULT_RULE_THICKNESS; + _measurementUnit = [_CPMeasurementUnit measurementUnitNamed:@"Inches"]; + _rulerlineLocations = []; + } + return self; +} + +// MARK: - Layout & Metrics + +- (float)reservedThicknessForMarkers +{ + if ([_markers count] > 0 && _thicknessForMarkers < DEFAULT_MARKER_THICKNESS) + return DEFAULT_MARKER_THICKNESS; + + return _thicknessForMarkers; +} + +- (float)reservedThicknessForAccessoryView +{ + return _thicknessForAccessoryView; +} + +- (float)ruleThickness +{ + return _ruleThickness; +} + +- (float)requiredThickness +{ + var result = [self ruleThickness]; + + if ([_markers count] > 0) + result += [self reservedThicknessForMarkers]; + + if (_accessoryView) + result += [self reservedThicknessForAccessoryView]; + + return result; +} + +- (float)baselineLocation +{ + // The baseline is the line separating the ruler from the content. + // In a horizontal ruler, it's the bottom edge (height). + // Usually, the hash marks grow upwards from this baseline. + return [self bounds].size.height; +} + +// MARK: - Accessors + +- (CPScrollView)scrollView +{ + return _scrollView; +} + +- (void)setScrollView:(CPScrollView)scrollView +{ + _scrollView = scrollView; + [self setNeedsDisplay:YES]; +} + +- (CPRulerOrientation)orientation +{ + return _orientation; +} + +- (void)setOrientation:(CPRulerOrientation)orientation +{ + _orientation = orientation; + [self setNeedsDisplay:YES]; +} + +- (CPView)clientView +{ + return _clientView; +} + +- (void)setClientView:(CPView)view +{ + if (_clientView === view) return; + + if ([_clientView respondsToSelector:@selector(rulerView:willSetClientView:)]) + [_clientView rulerView:self willSetClientView:view]; + + // Standard behavior: clear markers when client changes unless preserved + [_markers removeAllObjects]; + _clientView = view; + + [self invalidateHashMarks]; + [[self enclosingScrollView] tile]; +} + +- (CPView)accessoryView +{ + return _accessoryView; +} + +- (void)setAccessoryView:(CPView)view +{ + if (_accessoryView === view) return; + + [_accessoryView removeFromSuperview]; + _accessoryView = view; + + if (_accessoryView) + { + [self addSubview:_accessoryView]; + // Usually you'd set the frame here based on thickness + } + + [[self enclosingScrollView] tile]; +} + +- (CPArray)markers +{ + return _markers; +} + +- (void)setMarkers:(CPArray)markers +{ + if (_markers === markers) return; + _markers = [markers mutableCopy]; + [[self enclosingScrollView] tile]; + [self setNeedsDisplay:YES]; +} + +- (void)addMarker:(CPRulerMarker)marker +{ + [_markers addObject:marker]; + [marker setRulerView:self]; // Ensure marker knows its ruler + [[self enclosingScrollView] tile]; + [self setNeedsDisplay:YES]; +} + +- (void)removeMarker:(CPRulerMarker)marker +{ + [_markers removeObject:marker]; + [[self enclosingScrollView] tile]; + [self setNeedsDisplay:YES]; +} + +- (void)setMeasurementUnits:(CPString)unitName +{ + var unit = [_CPMeasurementUnit measurementUnitNamed:unitName]; + if (unit) + { + _measurementUnit = unit; + [self setNeedsDisplay:YES]; + } +} + +- (CPString)measurementUnits +{ + return [_measurementUnit name]; +} + +- (void)setOriginOffset:(float)value +{ + _originOffset = value; + [self setNeedsDisplay:YES]; +} + +- (float)originOffset +{ + return _originOffset; +} + +- (void)setRuleThickness:(float)value +{ + _ruleThickness = value; + [[self enclosingScrollView] tile]; +} + +- (void)setReservedThicknessForMarkers:(float)value +{ + _thicknessForMarkers = value; + [[self enclosingScrollView] tile]; +} + +- (void)setReservedThicknessForAccessoryView:(float)value +{ + _thicknessForAccessoryView = value; + [[self enclosingScrollView] tile]; +} + +// MARK: - Event Handling + +- (BOOL)trackMarker:(CPRulerMarker)marker withMouseEvent:(CPEvent)event +{ + // Convert event location to ruler coordinates + var point = [self convertPoint:[event locationInWindow] fromView:nil]; + + // Basic hit testing logic + if (CPPointInRect(point, [self bounds])) + { + // Marker implements trackMouse:adding: + if ([marker respondsToSelector:@selector(trackMouse:adding:)]) + { + [marker trackMouse:event adding:YES]; + } + [self setNeedsDisplay:YES]; + return YES; + } + + return NO; +} + +- (void)mouseDown:(CPEvent)event +{ + var point = [self convertPoint:[event locationInWindow] fromView:nil], + i = 0, count = [_markers count]; + + // 1. Check if an existing marker was clicked + for (; i < count; ++i) + { + var marker = [_markers objectAtIndex:i]; + if (CPPointInRect(point, [marker imageRectInRuler])) + { + [marker trackMouse:event adding:NO]; + [self setNeedsDisplay:YES]; + return; + } + } + + // 2. Delegate to Client View (e.g. to create a new guide/marker) + if ([_clientView respondsToSelector:@selector(rulerView:handleMouseDown:)]) + [_clientView rulerView:self handleMouseDown:event]; +} + +// MARK: - Drawing Support + +- (void)moveRulerlineFromLocation:(float)fromLocation toLocation:(float)toLocation +{ + var oldLoc = [CPNumber numberWithFloat:fromLocation], + newLoc = [CPNumber numberWithFloat:toLocation]; + + [_rulerlineLocations removeObject:oldLoc]; + + // Only add if it's not effectively "off" (using -1 as a convention for hiding) + if (toLocation >= 0) + [_rulerlineLocations addObject:newLoc]; + + [self setNeedsDisplay:YES]; +} + +- (void)invalidateHashMarks +{ + [self setNeedsDisplay:YES]; +} + +// MARK: - Drawing Implementation + +- (BOOL)isFlipped +{ + if (_orientation == CPHorizontalRuler) + return YES; // Horizontal rulers usually draw top-down or match standard flipped views + + // For vertical rulers, we usually want to match the document view's flip state + // so numbers increase going down if the document does. + if (_clientView) + return [_clientView isFlipped]; + + return YES; +} + +- (float)_drawingScale +{ + // Zoom support + var scale = 1.0, + docView = [_scrollView documentView]; + + if (docView && [docView superview]) + { + // Calculate scale based on bounds vs frame + var bounds = [docView bounds], + frame = [docView frame]; + + if (_orientation == CPHorizontalRuler) + scale = frame.size.width / bounds.size.width; + else + scale = frame.size.height / bounds.size.height; + } + return scale; +} + +- (float)_drawingOrigin +{ + // Calculate the point in the ruler that corresponds to "0" in the client view + var origin = 0.0, + trackedView = _clientView; + + if (!trackedView) + trackedView = [_scrollView documentView]; + + if (!trackedView) return 0.0; + + // Convert (0,0) of the tracked view to the ruler's coordinate space + var viewZero = [self convertPoint:CGPointMake(0,0) fromView:trackedView]; + + if (_orientation == CPHorizontalRuler) + origin = viewZero.x; + else + origin = viewZero.y; + + // Apply user-defined offset (e.g. if the user dragged the zero-point) + // Note: originOffset is in client coordinates, so we scale it. + origin += (_originOffset * [self _drawingScale]); + + return origin; +} + +- (void)drawHashMarksAndLabelsInRect:(CPRect)dirtyRect +{ + var bounds = [self bounds], + scale = [self _drawingScale], + zeroLocation = [self _drawingOrigin], // x position (horiz) or y position (vert) where 0 is + pointsPerUnit = [_measurementUnit pointsPerUnit] * scale; + + // Avoid divide by zero + if (pointsPerUnit <= 0) return; + + var isHorizontal = (_orientation == CPHorizontalRuler), + ruleSize = isHorizontal ? bounds.size.height : bounds.size.width; + + // Adjust for markers/accessory thickness area + var reserved = 0.0; + if ([_markers count] > 0) reserved += [self reservedThicknessForMarkers]; + if (_accessoryView) reserved += [self reservedThicknessForAccessoryView]; + + // The actual area for hashes + var hashAreaSize = ruleSize - reserved, + hashBaseline = reserved; // Drawing starts after reserved area + + // Calculate range of units to draw based on dirtyRect + var startPos = isHorizontal ? dirtyRect.origin.x : dirtyRect.origin.y, + endPos = isHorizontal ? CPReectGetMaxX(dirtyRect) : CPReectGetMaxY(dirtyRect); + + // Convert view coordinates to Unit coordinates + // pos = zeroLocation + (unit * pointsPerUnit) + // unit = (pos - zeroLocation) / pointsPerUnit + var startUnit = Math.floor((startPos - zeroLocation) / pointsPerUnit), + endUnit = Math.ceil((endPos - zeroLocation) / pointsPerUnit); + + var stepDowns = [_measurementUnit stepDownCycle], + numSteps = [stepDowns count]; + + [[CPColor grayColor] setStroke]; + + // Iterate through units + for (var u = startUnit; u <= endUnit; u++) + { + var unitPos = zeroLocation + (u * pointsPerUnit); + + // 1. Draw Major Mark + var majorHeight = hashAreaSize * HASH_MARK_THICKNESS_FACTOR; + [self _drawHashAt:unitPos length:majorHeight offset:hashBaseline horizontal:isHorizontal]; + + // 2. Draw Label (only for major units) + var labelStr = [CPString stringWithFormat:@"%d", u]; + + // Simple label positioning + var labelPoint; + if (isHorizontal) + labelPoint = CPMakePoint(unitPos + LABEL_TEXT_PADDING, hashBaseline); + else + labelPoint = CPMakePoint(hashBaseline + LABEL_TEXT_PADDING, unitPos); + + [labelStr drawAtPoint:labelPoint withAttributes:_labelAttributes]; + + // 3. Draw Subdivisions + // We handle a simple single-level subdivision for performance, + // or iterate the cycle. Let's do a simple iterative cycle. + + // E.g. stepDownCycle: [0.5, 0.25, 0.125] implies 1/2, then 1/4, etc. + // But the CPMeasurementUnit spec in the prompt suggests relative steps. + // Let's assume the array contains fractions of the unit: e.g. [0.5, 0.1] + + for (var s = 0; s < numSteps; s++) + { + var stepFraction = [[stepDowns objectAtIndex:s] floatValue]; + if (stepFraction <= 0) continue; + + // Determine visual spacing. If marks are too close, stop recursing. + var pixelsPerStep = pointsPerUnit * stepFraction; + if (pixelsPerStep < 4.0) break; + + // Calculate height: gradually smaller + var subHeight = majorHeight * (0.7 / (s + 1)); + + // How many marks fit in one unit? 1 / fraction. + // e.g. 0.5 -> 2 marks (at 0.0 and 0.5). 0.0 is major, so we draw at 0.5. + var subCount = Math.round(1.0 / stepFraction); + + for (var k = 1; k < subCount; k++) + { + // We only draw if this position wasn't covered by a higher-level step. + // Simplified: Just draw everything, simpler logic for UI. + // Optimization: Skip if integer check passes? No, keep it simple. + + var subUnitOffset = k * stepFraction; + // Only draw if this isn't a whole integer (covered by major) + if (subUnitOffset % 1.0 === 0) continue; + + var subPos = unitPos + (subUnitOffset * pointsPerUnit); + + // Bounds check optimization + if (subPos < startPos || subPos > endPos) continue; + + [self _drawHashAt:subPos length:subHeight offset:hashBaseline horizontal:isHorizontal]; + } + } + } +} + +- (void)_drawHashAt:(float)pos length:(float)length offset:(float)offset horizontal:(BOOL)isHorizontal +{ + var path = [CPBezierPath bezierPath]; + [path setLineWidth:1.0]; + + if (isHorizontal) + { + // Draw vertical line at 'pos' + [path moveToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height - length)]; // Draw from bottom up + [path lineToPoint:CPMakePoint(pos + 0.5, [self bounds].size.height)]; + + // Or if flipped (top-down), draw from offset down + // Since we force isFlipped=YES for horizontal, y=0 is top. + // Typically rulers align marks to the edge touching the content. + [path moveToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height - length)]; + [path lineToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height)]; + } + else + { + // Draw horizontal line at 'pos' + [path moveToPoint:CPMakePoint(offset + [self bounds].size.width - length, pos + 0.5)]; + [path lineToPoint:CPMakePoint(offset + [self bounds].size.width, pos + 0.5)]; + } + + [path stroke]; +} + +- (void)drawMarkersInRect:(CPRect)dirtyRect +{ + var count = [_markers count]; + for (var i = 0; i < count; i++) + { + var m = [_markers objectAtIndex:i]; + if (CPRectIntersectsRect([m imageRectInRuler], dirtyRect)) + [m drawRect:dirtyRect]; + } +} + +- (void)drawRulerlineLocationsInRect:(CPRect)rect +{ + var count = [_rulerlineLocations count]; + if (count === 0) return; + + [[CPColor controlShadowColor] setStroke]; + + var bounds = [self bounds]; + + for (var i = 0; i < count; ++i) + { + var loc = [[_rulerlineLocations objectAtIndex:i] floatValue]; + + var path = [CPBezierPath bezierPath]; + if (_orientation == CPHorizontalRuler) + { + [path moveToPoint:CPMakePoint(loc + 0.5, 0)]; + [path lineToPoint:CPMakePoint(loc + 0.5, bounds.size.height)]; + } + else + { + [path moveToPoint:CPMakePoint(0, loc + 0.5)]; + [path lineToPoint:CPMakePoint(bounds.size.width, loc + 0.5)]; + } + [path stroke]; + } +} + +- (void)drawRect:(CPRect)dirtyRect +{ + var context = [[CPGraphicsContext currentContext] graphicsPort]; + + // 1. Draw Background + [[CPColor colorWithCalibratedWhite:0.96 alpha:1.0] setFill]; + CGContextFillRect(context, dirtyRect); + + // 2. Draw Bottom/Right Border (Baseline) + [[CPColor darkGrayColor] setStroke]; + var borderPath = [CPBezierPath bezierPath]; + var bounds = [self bounds]; + + if (_orientation == CPHorizontalRuler) + { + [borderPath moveToPoint:CPMakePoint(0, bounds.size.height - 0.5)]; + [borderPath lineToPoint:CPMakePoint(bounds.size.width, bounds.size.height - 0.5)]; + } + else + { + [borderPath moveToPoint:CPMakePoint(bounds.size.width - 0.5, 0)]; + [borderPath lineToPoint:CPMakePoint(bounds.size.width - 0.5, bounds.size.height)]; + } + [borderPath stroke]; + + // 3. Draw Contents + [self drawHashMarksAndLabelsInRect:dirtyRect]; + + if ([_markers count] > 0) + [self drawMarkersInRect:dirtyRect]; + + if ([_rulerlineLocations count] > 0) + [self drawRulerlineLocationsInRect:dirtyRect]; +} + +@end + +@implementation CPRulerMarker : CPObject +{ + CPRulerView _ruler; + float _markerLocation; + CPImage _image; + CPPoint _imageOrigin; + + BOOL _movable @accessors(property=movable); + BOOL _removable @accessors(property=removable); + + id _representedObject @accessors(property=representedObject); + + // Internal state + BOOL _dragging @accessors(getter=isDragging); +} + +// MARK: - Initialization + +- (id)initWithRulerView:(CPRulerView)aRuler markerLocation:(float)location image:(CPImage)anImage imageOrigin:(CPPoint)anImageOrigin +{ + self = [super init]; + if (self) + { + _ruler = aRuler; + _markerLocation = location; + _image = anImage; + _imageOrigin = anImageOrigin; + + _movable = NO; + _removable = NO; + _dragging = NO; + } + return self; +} + +- (id)initWithCoder:(CPCoder)aCoder +{ + self = [super init]; + if (self) + { + // Ruler is usually assigned after decoding by the view hierarchy + _markerLocation = [aCoder decodeFloatForKey:@"CPMarkerLocation"]; + _image = [aCoder decodeObjectForKey:@"CPImage"]; + _imageOrigin = [aCoder decodePointForKey:@"CPImageOrigin"]; + _movable = [aCoder decodeBoolForKey:@"CPMovable"]; + _removable = [aCoder decodeBoolForKey:@"CPRemovable"]; + _representedObject = [aCoder decodeObjectForKey:@"CPRepresentedObject"]; + } + return self; +} + +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [aCoder encodeFloat:_markerLocation forKey:@"CPMarkerLocation"]; + [aCoder encodeObject:_image forKey:@"CPImage"]; + [aCoder encodePoint:_imageOrigin forKey:@"CPImageOrigin"]; + [aCoder encodeBool:_movable forKey:@"CPMovable"]; + [aCoder encodeBool:_removable forKey:@"CPRemovable"]; + [aCoder encodeObject:_representedObject forKey:@"CPRepresentedObject"]; +} + +- (id)copy +{ + var copy = [[CPRulerMarker alloc] initWithRulerView:_ruler + markerLocation:_markerLocation + image:_image + imageOrigin:_imageOrigin]; + [copy setMovable:_movable]; + [copy setRemovable:_removable]; + [copy setRepresentedObject:_representedObject]; + return copy; +} + +// MARK: - Accessors + +- (CPRulerView)ruler +{ + return _ruler; +} + +// Used when adding a marker to a ruler +- (void)setRulerView:(CPRulerView)aRuler +{ + _ruler = aRuler; +} + +- (CPImage)image +{ + return _image; +} + +- (void)setImage:(CPImage)anImage +{ + _image = anImage; + [_ruler setNeedsDisplay:YES]; +} + +- (CPPoint)imageOrigin +{ + return _imageOrigin; +} + +- (void)setImageOrigin:(CPPoint)aPoint +{ + _imageOrigin = aPoint; + [_ruler setNeedsDisplay:YES]; +} + +- (float)markerLocation +{ + return _markerLocation; +} + +- (void)setMarkerLocation:(float)location +{ + if (_markerLocation === location) return; + + _markerLocation = location; + + // Invalidate the ruler to redraw at new position + [_ruler setNeedsDisplay:YES]; +} + +// MARK: - Geometry + +- (float)thicknessRequiredInRuler +{ + if (!_image) return 0.0; + + // If horizontal ruler, thickness is image height. + // If vertical ruler, thickness is image width. + var size = [_image size]; + + if ([_ruler orientation] === CPHorizontalRuler) + return size.height; + else + return size.width; +} + +- (CPRect)imageRectInRuler +{ + if (!_ruler || !_image) return CPMakeRect(0,0,0,0); + + var clientView = [_ruler clientView]; + // If no client view, fallback to document view or 0 + if (!clientView) clientView = [[_ruler scrollView] documentView]; + + var rulerBounds = [_ruler bounds], + imageSize = [_image size], + rect = CPMakeRect(0, 0, imageSize.width, imageSize.height); + + // 1. Convert Marker Location (Client Coords) to Ruler Coords + var locationInRuler = 0.0; + + if (clientView) + { + // We create a point in the client view at the marker location + var ptInClient = CPMakePoint(0, 0); + + if ([_ruler orientation] === CPHorizontalRuler) + ptInClient.x = _markerLocation; + else + ptInClient.y = _markerLocation; + + // Convert that point to the ruler's coordinate system + var ptInRuler = [_ruler convertPoint:ptInClient fromView:clientView]; + + locationInRuler = ([_ruler orientation] === CPHorizontalRuler) ? ptInRuler.x : ptInRuler.y; + } + + // Apply user offset from CPRulerView (originOffset) + // Note: The conversion above usually handles view transforms, but if CPRulerView + // manually applies an extra offset property (like in the previous implementation), + // we should respect it implicitly via the conversion or explicitly here. + // Assuming standard view conversion handles the scroll/bounds. + + // 2. Position the rect based on Orientation and Image Origin + // imageOrigin is the point *inside* the image that aligns with 'locationInRuler' + + if ([_ruler orientation] === CPHorizontalRuler) + { + // X: Aligned by location minus the hotspot X + rect.origin.x = locationInRuler - _imageOrigin.x; + + // Y: This depends on the ruler's baseline. + // Typically horizontal markers sit on the bottom edge (baseline). + // If the image origin Y is the "tip", we position relative to that. + // Usually, imageOrigin.y is 0 (bottom) or Height (top) depending on coordinate flip. + + // Assuming the ruler draws labels near the baseline and the marker sits on it: + // Let's assume the baseline is the bottom of the ruler rect. + var baseline = [_ruler baselineLocation]; + rect.origin.y = baseline - _imageOrigin.y; + + // If flipped, adjustments might be needed, but usually image drawing handles internal flip. + } + else + { + // Vertical Ruler + // Y: Aligned by location minus hotspot Y + rect.origin.y = locationInRuler - _imageOrigin.y; + + // X: Align to right edge (baseline) + var baseline = [_ruler baselineLocation]; // Width for vertical ruler + rect.origin.x = baseline - _imageOrigin.x; + } + + return rect; +} + +// MARK: - Drawing + +- (void)drawRect:(CPRect)aRect +{ + if (!_image) return; + + var rect = [self imageRectInRuler]; + + // Only draw if visible + if (CPRectIntersectsRect(rect, aRect)) + { + // Visual feedback for dragging + var opacity = _dragging ? 0.5 : 1.0; + + [_image drawInRect:rect fromRect:CPZeroRect operation:CPCompositeSourceOver fraction:opacity]; + } +} + +// MARK: - Event Handling + +- (BOOL)trackMouse:(CPEvent)anEvent adding:(BOOL)adding +{ + if (!adding && !_movable) return NO; + + var clientView = [_ruler clientView], + delegate = clientView; // The client view acts as the delegate usually + + // Delegate Check: Should we move/add? + if (adding) + { + if ([delegate respondsToSelector:@selector(rulerView:shouldAddMarker:)] && + ![delegate rulerView:_ruler shouldAddMarker:self]) + return NO; + } + else + { + if ([delegate respondsToSelector:@selector(rulerView:shouldMoveMarker:)] && + ![delegate rulerView:_ruler shouldMoveMarker:self]) + return NO; + } + + // If adding, we ensure the marker is in the array so it draws + if (adding) + [_ruler addMarker:self]; + + [self setDragging:YES]; + + var type = [anEvent type], + originalLocation = _markerLocation; + + // Start Event Loop + while (type !== CPLeftMouseUp) + { + // 1. Calculate new location + var locationInWindow = [anEvent locationInWindow], + pointInClient = [clientView convertPoint:locationInWindow fromView:nil], + newLocation = ([_ruler orientation] === CPHorizontalRuler) ? pointInClient.x : pointInClient.y; + + // 2. Delegate: Will Move? (Snap logic usually happens here) + if ([delegate respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)]) + { + newLocation = [delegate rulerView:_ruler willMoveMarker:self toLocation:newLocation]; + } + + // 3. Update Location + [self setMarkerLocation:newLocation]; + + // 4. Handle "Tearing off" (Removal visual feedback) + // Check if mouse is far outside the ruler's bounds + var pointInRuler = [_ruler convertPoint:locationInWindow fromView:nil]; + var rulerBounds = [_ruler bounds]; + // Expand bounds slightly for tolerance + var expandedBounds = CPMakeRect(rulerBounds.origin.x - 20, rulerBounds.origin.y - 20, + rulerBounds.size.width + 40, rulerBounds.size.height + 40); + + var isFarAway = !CPPointInRect(pointInRuler, expandedBounds); + + // You might change the cursor here to a "poof" cursor if isFarAway && _removable + + // 5. Get next event + anEvent = [[CPApp currentEvent] window] ? [[CPApp currentEvent] window] : [CPApp keyWindow]; // fallback + anEvent = [CPApp nextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask + untilDate:[CPDate distantFuture] + inMode:CPDefaultRunLoopMode + dequeue:YES]; + type = [anEvent type]; + } + + [self setDragging:NO]; + + // Finalize + var pointInRuler = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil], + rulerBounds = [_ruler bounds], + expandedBounds = CPMakeRect(rulerBounds.origin.x - 10, rulerBounds.origin.y - 10, + rulerBounds.size.width + 20, rulerBounds.size.height + 20), + shouldRemove = _removable && !CPPointInRect(pointInRuler, expandedBounds); + + if (shouldRemove) + { + var allowed = YES; + if ([delegate respondsToSelector:@selector(rulerView:shouldRemoveMarker:)]) + allowed = [delegate rulerView:_ruler shouldRemoveMarker:self]; + + if (allowed) + { + [_ruler removeMarker:self]; + // Don't call didMove or didAdd if removed + return YES; + } + } + + // Success notification + if (adding) + { + if ([delegate respondsToSelector:@selector(rulerView:didAddMarker:)]) + [delegate rulerView:_ruler didAddMarker:self]; + } + else + { + if ([delegate respondsToSelector:@selector(rulerView:didMoveMarker:)]) + [delegate rulerView:_ruler didMoveMarker:self]; + } + + return YES; +} + +@end From 5aa315e19057d09c9423fbc5232b3f98cd00520b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 16 Jan 2026 17:17:00 +0100 Subject: [PATCH 02/39] fixed: syntax errors --- AppKit/CPTextView/CPParagraphStyle.j | 417 +++++++++++++++++---------- 1 file changed, 259 insertions(+), 158 deletions(-) diff --git a/AppKit/CPTextView/CPParagraphStyle.j b/AppKit/CPTextView/CPParagraphStyle.j index 54d88ee73..c6d2c1b71 100644 --- a/AppKit/CPTextView/CPParagraphStyle.j +++ b/AppKit/CPTextView/CPParagraphStyle.j @@ -2,10 +2,6 @@ * CPParagraphStyle.j * AppKit * - * FIXME - * This is basically a stub. - * We need to store all the spacing informations as well as writing direction (among others) - * * Created by Daniel Boehringer on 11/01/2014 * Copyright Daniel Boehringer 2014. * @@ -26,198 +22,303 @@ @import @import +@import -@import "CPText.j" +var CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName"; -CPLeftTabStopType = 0; +// Standard Tab Interval (28pts is roughly 4 spaces in standard fonts) +var kDefaultTabInterval = 28.0; -CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName"; +// MARK: - CPTextTab Implementation -var _sharedDefaultParagraphStyle, - _defaultTabStopArray; - -@implementation CPParagraphStyle : CPObject +@implementation CPTextTab : CPObject { - CPArray _tabStops @accessors(property=tabStops); - CPTextAlignment _alignment @accessors(property=alignment); - unsigned _firstLineHeadIndent @accessors(property=firstLineHeadIndent); - unsigned _headIndent @accessors(property=headIndent); - unsigned _tailIndent @accessors(property=tailIndent); - unsigned _paragraphSpacing @accessors(property=paragraphSpacing); - unsigned _minimumLineHeight @accessors(property=minimumLineHeight); - unsigned _maximumLineHeight @accessors(property=maximumLineHeight); - unsigned _lineSpacing @accessors(property=lineSpacing); + CPTextAlignment _alignment @accessors(readonly, property=alignment); + float _location @accessors(readonly, property=location); + CPDictionary _options @accessors(readonly, property=options); } - -#pragma mark - -#pragma mark Class methods - -+ (CPParagraphStyle)defaultParagraphStyle +- (id)initWithTextAlignment:(CPTextAlignment)anAlignment location:(float)aLocation options:(CPDictionary)options { - if (!_sharedDefaultParagraphStyle) - _sharedDefaultParagraphStyle = [self new]; - - return _sharedDefaultParagraphStyle; -} - -+ (CPArray)_defaultTabStops -{ - if (!_defaultTabStopArray) + if (self = [super init]) { - var i; - _defaultTabStopArray = []; - - // FIXME: Define constants for these magic numbers: 13, 28 - for (i = 1; i < 16 ; i++) - { - _defaultTabStopArray.push([[CPTextTab alloc] initWithType:CPLeftTabStopType location:i * 28]); - } + _alignment = anAlignment; + _location = aLocation; + _options = [options copy]; } - - return _defaultTabStopArray; -} - - -#pragma mark - -#pragma mark Init methods - -- (id)init -{ - [self _initWithDefaults]; - return self; } -- (CPParagraphStyle)initWithParagraphStyle:(CPParagraphStyle)other +// Convenience initializer matching AppKit behavior +- (id)initWithType:(CPTabStopType)aType location:(float)aLocation { - self = [super init]; - - _tabStops = [other._tabStops copy]; - _alignment = other._alignment; - _firstLineHeadIndent = other._firstLineHeadIndent; - _headIndent = other._headIndent; - _tailIndent = other._tailIndent; - _paragraphSpacing = other._paragraphSpacing; - _minimumLineHeight = other._minimumLineHeight; - _maximumLineHeight = other._maximumLineHeight; - _lineSpacing = other._lineSpacing; - - return self; + // Map old TabStopType to TextAlignment for modern compatibility + return [self initWithTextAlignment:aType location:aLocation options:nil]; } -- (void)_initWithDefaults +- (BOOL)isEqual:(id)other { - _alignment = CPLeftTextAlignment; - _tabStops = [[[self class] _defaultTabStops] copy]; -} + if (self === other) return YES; + if (![other isKindOfClass:[CPTextTab class]]) return NO; -- (void)addTabStop:(CPTextTab)aStop -{ - _tabStops.push(aStop); + return _location === [other location] && + _alignment === [other alignment] && + ((_options == nil && [other options] == nil) || [_options isEqualToDictionary:[other options]]); } - (id)copy { - var other = [[self class] alloc]; + return [[CPTextTab alloc] initWithTextAlignment:_alignment location:_location options:_options]; +} - return [other initWithParagraphStyle:self]; +- (id)initWithCoder:(CPCoder)aCoder +{ + if (self = [super init]) + { + _alignment = [aCoder decodeIntForKey:@"CPTextTabAlignment"]; + _location = [aCoder decodeFloatForKey:@"CPTextTabLocation"]; + _options = [aCoder decodeObjectForKey:@"CPTextTabOptions"]; + } + return self; +} + +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [aCoder encodeInt:_alignment forKey:@"CPTextTabAlignment"]; + [aCoder encodeFloat:_location forKey:@"CPTextTabLocation"]; + [aCoder encodeObject:_options forKey:@"CPTextTabOptions"]; } @end -var CPParagraphStyleTabStopsKey = @"CPParagraphStyleTabStopsKey", - CPParagraphStyleAlignmentKey = @"CPParagraphStyleAlignmentKey", - CPParagraphStyleFirstLineHeadIndentKey = @"CPParagraphStyleFirstLineHeadIndentKey", - CPParagraphStyleHeadIndentKey = @"CPParagraphStyleHeadIndentKey", - CPParagraphStyleTailIndentKey = @"CPParagraphStyleTailIndentKey", - CPParagraphStyleParagraphSpacingKey = @"CPParagraphStyleParagraphSpacingKey", - CPParagraphStyleMinimumLineHeightKey = @"CPParagraphStyleMinimumLineHeightKey", - CPParagraphStyleMaximumLineHeightKey = @"CPParagraphStyleMaximumLineHeightKey", - CPParagraphStyleLineSpacingKey = @"CPParagraphStyleLineSpacingKey"; +// MARK: - CPParagraphStyle Implementation + +var _sharedDefaultParagraphStyle = nil; + +@implementation CPParagraphStyle : CPObject +{ + float _lineSpacing @accessors(readonly, property=lineSpacing); + float _paragraphSpacing @accessors(readonly, property=paragraphSpacing); + CPTextAlignment _alignment @accessors(readonly, property=alignment); + float _headIndent @accessors(readonly, property=headIndent); + float _tailIndent @accessors(readonly, property=tailIndent); + float _firstLineHeadIndent @accessors(readonly, property=firstLineHeadIndent); + float _minimumLineHeight @accessors(readonly, property=minimumLineHeight); + float _maximumLineHeight @accessors(readonly, property=maximumLineHeight); + CPLineBreakMode _lineBreakMode @accessors(readonly, property=lineBreakMode); + CPWritingDirection _baseWritingDirection @accessors(readonly, property=baseWritingDirection); + float _lineHeightMultiple @accessors(readonly, property=lineHeightMultiple); + float _paragraphSpacingBefore @accessors(readonly, property=paragraphSpacingBefore); + float _defaultTabInterval @accessors(readonly, property=defaultTabInterval); + CPArray _tabStops @accessors(readonly, property=tabStops); +} + ++ (CPParagraphStyle)defaultParagraphStyle +{ + if (!_sharedDefaultParagraphStyle) + { + _sharedDefaultParagraphStyle = [[CPParagraphStyle alloc] init]; + // Ensure defaults are set on the shared instance internal vars + // Since it's immutable, we rely on the init to set these. + } + return _sharedDefaultParagraphStyle; +} + ++ (CPWritingDirection)defaultWritingDirectionForLanguage:(CPString)languageName +{ + // Simplified: Cappuccino usually assumes LTR unless specified otherwise. + return CPWritingDirectionLeftToRight; +} + +- (id)init +{ + if (self = [super init]) + { + _lineSpacing = 0.0; + _paragraphSpacing = 0.0; + _alignment = CPLeftTextAlignment; + _headIndent = 0.0; + _tailIndent = 0.0; + _firstLineHeadIndent = 0.0; + _minimumLineHeight = 0.0; + _maximumLineHeight = 0.0; + _lineBreakMode = CPLineBreakByWordWrapping; + _baseWritingDirection = CPWritingDirectionNatural; + _lineHeightMultiple = 0.0; + _paragraphSpacingBefore = 0.0; + _defaultTabInterval = kDefaultTabInterval; + + // Generate default tab stops + _tabStops = []; + for (var i = 1; i <= 12; i++) + { + [_tabStops addObject:[[CPTextTab alloc] initWithType:CPLeftTextAlignment + location:i * kDefaultTabInterval]]; + } + } + return self; +} + +- (id)initWithParagraphStyle:(CPParagraphStyle)other +{ + if (self = [super init]) + { + _lineSpacing = [other lineSpacing]; + _paragraphSpacing = [other paragraphSpacing]; + _alignment = [other alignment]; + _headIndent = [other headIndent]; + _tailIndent = [other tailIndent]; + _firstLineHeadIndent = [other firstLineHeadIndent]; + _minimumLineHeight = [other minimumLineHeight]; + _maximumLineHeight = [other maximumLineHeight]; + _lineBreakMode = [other lineBreakMode]; + _baseWritingDirection = [other baseWritingDirection]; + _lineHeightMultiple = [other lineHeightMultiple]; + _paragraphSpacingBefore = [other paragraphSpacingBefore]; + _defaultTabInterval = [other defaultTabInterval]; + _tabStops = [[other tabStops] copy]; + } + return self; +} + +- (id)copy +{ + // Since this class is immutable, return self. + // Subclasses (Mutable) will override. + if ([self class] === [CPParagraphStyle class]) + return self; + + return [[CPParagraphStyle alloc] initWithParagraphStyle:self]; +} + +- (id)mutableCopy +{ + return [[CPMutableParagraphStyle alloc] initWithParagraphStyle:self]; +} + +// MARK: - Equality + +- (BOOL)isEqual:(id)other +{ + if (self === other) return YES; + if (![other isKindOfClass:[CPParagraphStyle class]]) return NO; + + return _lineSpacing === [other lineSpacing] && + _paragraphSpacing === [other paragraphSpacing] && + _alignment === [other alignment] && + _headIndent === [other headIndent] && + _tailIndent === [other tailIndent] && + _firstLineHeadIndent === [other firstLineHeadIndent] && + _lineBreakMode === [other lineBreakMode] && + [_tabStops isEqualToArray:[other tabStops]]; +} + +@end + + +// MARK: - CPMutableParagraphStyle Implementation + +@implementation CPMutableParagraphStyle : CPParagraphStyle +{ +} + + +- (void)addTabStop:(CPTextTab)aTabStop +{ + // Copy on write logic would go here if we shared structure, + // but here we just mutate the array. + [_tabStops addObject:aTabStop]; +} + +- (void)setTabStops:(CPArray)newTabStops +{ + if (_tabStops === newTabStops) return; + _tabStops = [newTabStops copy]; +} + +- (id)copyWithZone:(CPZone)aZone +{ + // Return an immutable copy + return [[CPParagraphStyle alloc] initWithParagraphStyle:self]; +} + +@end + + +// MARK: - CPCoding + +var CPParagraphStyleLineSpacingKey = @"CPParagraphStyleLineSpacingKey", + CPParagraphStyleParagraphSpacingKey = @"CPParagraphStyleParagraphSpacingKey", + CPParagraphStyleAlignmentKey = @"CPParagraphStyleAlignmentKey", + CPParagraphStyleHeadIndentKey = @"CPParagraphStyleHeadIndentKey", + CPParagraphStyleTailIndentKey = @"CPParagraphStyleTailIndentKey", + CPParagraphStyleFirstLineHeadIndentKey = @"CPParagraphStyleFirstLineHeadIndentKey", + CPParagraphStyleMinimumLineHeightKey = @"CPParagraphStyleMinimumLineHeightKey", + CPParagraphStyleMaximumLineHeightKey = @"CPParagraphStyleMaximumLineHeightKey", + CPParagraphStyleLineBreakModeKey = @"CPParagraphStyleLineBreakModeKey", + CPParagraphStyleTabStopsKey = @"CPParagraphStyleTabStopsKey", + CPParagraphStyleBaseWritingDirectionKey = @"CPParagraphStyleBaseWritingDirectionKey", + CPParagraphStyleLineHeightMultipleKey = @"CPParagraphStyleLineHeightMultipleKey", + CPParagraphStyleParagraphSpacingBeforeKey = @"CPParagraphStyleParagraphSpacingBeforeKey", + CPParagraphStyleDefaultTabIntervalKey = @"CPParagraphStyleDefaultTabIntervalKey"; @implementation CPParagraphStyle (CPCoding) -- (id)initWithCoder:(id)aCoder +- (id)initWithCoder:(CPCoder)aCoder { - self = [self init]; - - if (self) + if (self = [super init]) { - _tabStops = [aCoder decodeObjectForKey:"CPParagraphStyleTabStopsKey"]; - _alignment = [aCoder decodeIntForKey:"CPParagraphStyleAlignmentKey"]; - _firstLineHeadIndent = [aCoder decodeIntForKey:"CPParagraphStyleFirstLineHeadIndentKey"]; - _headIndent = [aCoder decodeIntForKey:"CPParagraphStyleHeadIndentKey"]; - _tailIndent = [aCoder decodeIntForKey:"CPParagraphStyleTailIndentKey"]; - _paragraphSpacing = [aCoder decodeIntForKey:"CPParagraphStyleParagraphSpacingKey"]; - _minimumLineHeight = [aCoder decodeIntForKey:"CPParagraphStyleMinimumLineHeightKey"]; - _maximumLineHeight = [aCoder decodeIntForKey:"CPParagraphStyleMaximumLineHeightKey"]; - _lineSpacing = [aCoder decodeIntForKey:"CPParagraphStyleLineSpacingKey"]; + _lineSpacing = [aCoder decodeFloatForKey:CPParagraphStyleLineSpacingKey]; + _paragraphSpacing = [aCoder decodeFloatForKey:CPParagraphStyleParagraphSpacingKey]; + _alignment = [aCoder decodeIntForKey:CPParagraphStyleAlignmentKey]; + _headIndent = [aCoder decodeFloatForKey:CPParagraphStyleHeadIndentKey]; + _tailIndent = [aCoder decodeFloatForKey:CPParagraphStyleTailIndentKey]; + _firstLineHeadIndent = [aCoder decodeFloatForKey:CPParagraphStyleFirstLineHeadIndentKey]; + _minimumLineHeight = [aCoder decodeFloatForKey:CPParagraphStyleMinimumLineHeightKey]; + _maximumLineHeight = [aCoder decodeFloatForKey:CPParagraphStyleMaximumLineHeightKey]; + _lineBreakMode = [aCoder decodeIntForKey:CPParagraphStyleLineBreakModeKey]; + _tabStops = [aCoder decodeObjectForKey:CPParagraphStyleTabStopsKey]; + + // Handle potential missing keys for backward compatibility or new properties + if ([aCoder containsValueForKey:CPParagraphStyleBaseWritingDirectionKey]) + _baseWritingDirection = [aCoder decodeIntForKey:CPParagraphStyleBaseWritingDirectionKey]; + else + _baseWritingDirection = CPWritingDirectionNatural; + + if ([aCoder containsValueForKey:CPParagraphStyleLineHeightMultipleKey]) + _lineHeightMultiple = [aCoder decodeFloatForKey:CPParagraphStyleLineHeightMultipleKey]; + + if ([aCoder containsValueForKey:CPParagraphStyleParagraphSpacingBeforeKey]) + _paragraphSpacingBefore = [aCoder decodeFloatForKey:CPParagraphStyleParagraphSpacingBeforeKey]; + + if ([aCoder containsValueForKey:CPParagraphStyleDefaultTabIntervalKey]) + _defaultTabInterval = [aCoder decodeFloatForKey:CPParagraphStyleDefaultTabIntervalKey]; + else + _defaultTabInterval = kDefaultTabInterval; + + if (!_tabStops) _tabStops = []; } - return self; } -- (void)encodeWithCoder:(id)aCoder +- (void)encodeWithCoder:(CPCoder)aCoder { - [aCoder encodeInt:_alignment forKey:"CPParagraphStyleAlignmentKey"]; - [aCoder encodeObject:_tabStops forKey:"CPParagraphStyleTabStopsKey"]; - [aCoder encodeInt:_firstLineHeadIndent forKey:"CPParagraphStyleFirstLineHeadIndentKey"]; - [aCoder encodeInt:_headIndent forKey:"CPParagraphStyleHeadIndentKey"]; - [aCoder encodeInt:_tailIndent forKey:"CPParagraphStyleTailIndentKey"]; - [aCoder encodeInt:_paragraphSpacing forKey:"CPParagraphStyleParagraphSpacingKey"]; - [aCoder encodeInt:_minimumLineHeight forKey:"CPParagraphStyleMinimumLineHeightKey"]; - [aCoder encodeInt:_maximumLineHeight forKey:"CPParagraphStyleMaximumLineHeightKey"]; - [aCoder encodeInt:_lineSpacing forKey:"CPParagraphStyleLineSpacingKey"]; -} - - -@end - - -@implementation CPTextTab : CPObject -{ - int _type @accessors(property = tabStopType); - double _location @accessors(property = location); -} - -- (id)initWithType:(CPTabStopType) aType location:(double) aLocation -{ - if ([self = [super init]]) - { - _type = aType; - _location = aLocation; - } - - return self; -} - -@end - - -var CPTextTabTypeKey = @"CPTextTabTypeKey", - CPTextTabLocationKey = @"CPTextTabLocationKey"; - -@implementation CPTextTab (CPCoding) - -- (id)initWithCoder:(id)aCoder -{ - self = [self init]; - - if (self) - { - _type = [aCoder decodeIntForKey:"CPTextTabTypeKey"]; - _location = [aCoder decodeDoubleForKey:"CPTextTabLocationKey"]; - } - - return self; -} - -- (void)encodeWithCoder:(id)aCoder -{ - [aCoder encodeInt:_type forKey:"CPTextTabTypeKey"]; - [aCoder encodeDouble:_location forKey:"CPTextTabLocationKey"]; + [aCoder encodeFloat:_lineSpacing forKey:CPParagraphStyleLineSpacingKey]; + [aCoder encodeFloat:_paragraphSpacing forKey:CPParagraphStyleParagraphSpacingKey]; + [aCoder encodeInt:_alignment forKey:CPParagraphStyleAlignmentKey]; + [aCoder encodeFloat:_headIndent forKey:CPParagraphStyleHeadIndentKey]; + [aCoder encodeFloat:_tailIndent forKey:CPParagraphStyleTailIndentKey]; + [aCoder encodeFloat:_firstLineHeadIndent forKey:CPParagraphStyleFirstLineHeadIndentKey]; + [aCoder encodeFloat:_minimumLineHeight forKey:CPParagraphStyleMinimumLineHeightKey]; + [aCoder encodeFloat:_maximumLineHeight forKey:CPParagraphStyleMaximumLineHeightKey]; + [aCoder encodeInt:_lineBreakMode forKey:CPParagraphStyleLineBreakModeKey]; + [aCoder encodeObject:_tabStops forKey:CPParagraphStyleTabStopsKey]; + + [aCoder encodeInt:_baseWritingDirection forKey:CPParagraphStyleBaseWritingDirectionKey]; + [aCoder encodeFloat:_lineHeightMultiple forKey:CPParagraphStyleLineHeightMultipleKey]; + [aCoder encodeFloat:_paragraphSpacingBefore forKey:CPParagraphStyleParagraphSpacingBeforeKey]; + [aCoder encodeFloat:_defaultTabInterval forKey:CPParagraphStyleDefaultTabIntervalKey]; } @end From d0d7033f6384384c59f0be9ad4ceb9a8cbd3591e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 14:53:44 +0100 Subject: [PATCH 03/39] Fixed: timing of CPControlTextDidBeginEditingNotification (#1941) --- AppKit/CPTextField.j | 6 ++++++ AppKit/CPTokenField.j | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j index 03416af31..741863b6d 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -1131,6 +1131,12 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); if (![self isEnabled] || !([self isEditable] || [self isSelectable])) return; + if ([self isEditable] && !_isEditing) + { + _isEditing = YES; + [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; + } + // CPTextField uses an HTML input element to take the input so we need to // propagate the dom event so the element is updated. This has to be done // before interpretKeyEvents: though so individual commands have a chance diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 61ed7d3dd..1de22bebd 100644 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -453,8 +453,8 @@ CPTokenFieldDeleteButtonType = 1; { [_tokenScrollView documentView]._DOMElement.appendChild(element); - //post CPControlTextDidBeginEditingNotification - [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; + // Removed so CPTokenField doesn't fire the notification the moment it becomes the first responder, but instead defers to the first keystroke, just like Cocoa (see keyDown: in CPTextField). + // [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; [[CPRunLoop mainRunLoop] performBlock:function() { From 4fd99d2c52b377fc74ff047d7a019e390882c439 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 14 Mar 2026 19:25:50 +0100 Subject: [PATCH 04/39] fixed: tokenfield CPControlTextDidBeginEditingNotification notification --- AppKit/CPTokenField.j | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 1de22bebd..71fcdde9a 100644 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -349,6 +349,12 @@ CPTokenFieldDeleteButtonType = 1; if (theBinding) [theBinding reverseSetValueFor:@"objectValue"]; + if (!_isEditing) + { + _isEditing = YES; + [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; + } + [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]]; _shouldNotifyTarget = YES; @@ -498,6 +504,8 @@ CPTokenFieldDeleteButtonType = 1; [self _resignFirstKeyResponder]; + _isEditing = NO; + if (_shouldNotifyTarget) { _shouldNotifyTarget = NO; @@ -1032,6 +1040,16 @@ CPTokenFieldDeleteButtonType = 1; CPTokenFieldTextDidChangeValue = [self stringValue]; #endif + // Has to be enabled, and it also has to be editable or selectable. + if (![self isEnabled] || !([self isEditable] || [self isSelectable])) + return; + + if ([self isEditable] && !_isEditing) + { + _isEditing = YES; + [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; + } + // Leave the default _propagateCurrentDOMEvent setting in place. This might be YES or NO depending // on if something that could be a browser shortcut was pressed or not, such as Cmd-R to reload. // If it was NO we want to leave it at NO however and only enable it in insertText:. This is what From 480917eeab650b7e542eb15e8870f0a691f936a2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 6 Apr 2026 13:15:55 +0200 Subject: [PATCH 05/39] new: doubleClickTarget and doubleClickArgument for CPTableView --- AppKit/CPTableView.j | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index 7a1ea6129..4a1949984 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -296,6 +296,8 @@ CPTableViewFirstColumnOnlyAutoresizingStyle = 5; _CPTableDrawView _tableDrawView; SEL _doubleAction; + id _doubleClickTarget @accessors(property=doubleClickTarget); + id _doubleClickArgument @accessors(property=doubleClickArgument); CPInteger _clickedRow; CPInteger _clickedColumn; unsigned _columnAutoResizingStyle; @@ -4734,7 +4736,12 @@ Your delegate can implement this method to avoid subclassing the tableview to ad //double click actions if ([[CPApp currentEvent] clickCount] === 2 && _doubleAction) - [self sendAction:_doubleAction to:_target]; + { + var target = _doubleClickTarget || _target, + argument = [self infoForBinding:@"doubleClickArgument"] ? _doubleClickArgument : self; + + [CPApp sendAction:_doubleAction to:target from:argument]; + } } /* @@ -6098,6 +6105,11 @@ Your delegate can implement this method to avoid subclassing the tableview to ad { if (aBinding == @"content") _contentBindingExplicitlySet = YES; + else if (aBinding == @"doubleClickTarget") + { + if ([options objectForKey:CPSelectorNameBindingOption]) + [self setDoubleAction:CPSelectorFromString([options objectForKey:CPSelectorNameBindingOption])]; + } [super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options]; } From 57987b6f6760eb57610db0e6b5698670bef51514 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 6 Apr 2026 17:26:38 +0200 Subject: [PATCH 06/39] Fixed: CPTableView bug where removing a column breaks internal state --- AppKit/CPTableView.j | 70 +++++++++++++++++-- .../Manual/TableTest/OldTest/AppController.j | 18 +++-- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index 7a1ea6129..bef6e7d4d 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -3486,13 +3486,73 @@ Your delegate can implement this method to avoid subclassing the tableview to ad [removeIndexes addIndex:columnIdx]; } - var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])]; - [self _unloadDataViewsInRows:rowIndexes columns:removeIndexes]; + if ([removeIndexes count] > 0) + { + var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])]; + [self _unloadDataViewsInRows:rowIndexes columns:removeIndexes]; - [_tableColumns removeObjectsAtIndexes:removeIndexes]; + [_tableColumns removeObjectsAtIndexes:removeIndexes]; - _dirtyTableColumnRangeIndex = 0; - [self _recalculateTableColumnRanges]; + _dirtyTableColumnRangeIndex = 0; + [self _recalculateTableColumnRanges]; + + // Shift cached index sets downwards to account for the removed columns + var shiftIndexSet = function(indexSet) + { + var newSet = [CPIndexSet indexSet]; + [indexSet enumerateIndexesUsingBlock:function(idx, stop) + { + if (![removeIndexes containsIndex:idx]) + { + var shift = 0, + remIdx = [removeIndexes firstIndex]; + + while (remIdx !== CPNotFound && remIdx < idx) + { + shift++; + remIdx = [removeIndexes indexGreaterThanIndex:remIdx]; + } + + [newSet addIndex:idx - shift]; + } + }]; + return newSet; + }; + + _exposedColumns = shiftIndexSet(_exposedColumns); + _selectedColumnIndexes = shiftIndexSet(_selectedColumnIndexes); + + // Shift individual index variables + var shiftIndex = function(idx) + { + if (idx === CPNotFound || idx === -1) + return idx; + + if ([removeIndexes containsIndex:idx]) + return CPNotFound; + + var shift = 0, + remIdx = [removeIndexes firstIndex]; + + while (remIdx !== CPNotFound && remIdx < idx) + { + shift++; + remIdx = [removeIndexes indexGreaterThanIndex:remIdx]; + } + + return idx - shift; + }; + + _editingColumn = shiftIndex(_editingColumn); + + _draggedColumnIndex = shiftIndex(_draggedColumnIndex); + if (_draggedColumnIndex === CPNotFound) + _draggedColumnIndex = -1; + + _clickedColumn = shiftIndex(_clickedColumn); + if (_clickedColumn === CPNotFound) + _clickedColumn = -1; + } [_differedColumnDataToRemove removeAllObjects]; _needsDifferedTableColumnRemove = NO; diff --git a/Tests/Manual/TableTest/OldTest/AppController.j b/Tests/Manual/TableTest/OldTest/AppController.j index d28b73b64..8c8d4741f 100644 --- a/Tests/Manual/TableTest/OldTest/AppController.j +++ b/Tests/Manual/TableTest/OldTest/AppController.j @@ -164,10 +164,20 @@ tableTestDragType = @"CPTableViewTestDragType"; - (void)removeColumn:(id)sender { - // if ([[tableView tableColumns] containsObject:randomColumn]) - [tableView removeTableColumn:randomColumn]; - //else - // [tableView addTableColumn:randomColumn]; + var columns = [tableView tableColumns]; + + // Check if we still have columns left to remove + if (columns && [columns count] > 0) + { + var columnToRemove = [columns lastObject]; + [tableView removeTableColumn:columnToRemove]; + + CPLog.debug(@"Removed column with identifier: " + [columnToRemove identifier]); + } + else + { + CPLog.debug(@"No more columns to remove!"); + } } - (void)addColumn:(id)sender From 872af9ce2a8d8f68f2cde84f03a85048978aa240 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 6 May 2026 19:54:43 +0200 Subject: [PATCH 07/39] Revise Cappuccino introduction and project status Reorganized and condensed introductory information about Cappuccino, emphasizing its purpose and benefits for building desktop-class applications. --- README.markdown | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.markdown b/README.markdown index 859570108..de4a02829 100644 --- a/README.markdown +++ b/README.markdown @@ -3,10 +3,6 @@ # Cappuccino: Build Desktop-Class Web Applications -Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. - -Cappuccino faithfully implements the proven design patterns of NeXTSTEP/Apple's Cocoa frameworks, enabling the creation of incredibly complex and reliable applications with a fraction of the code. - > **✨ Project Status: Active Development & Node.js Transition** > Cappuccino has been under continuous development since 2008 and is actively maintained. A major transition to a modern, **Node.js-based toolchain** has recently been finalized. The current release is a production-ready Release Candidate, with a formal release scheduled for 2026. It is stable, fast, and ready for new projects. @@ -14,7 +10,7 @@ Cappuccino faithfully implements the proven design patterns of NeXTSTEP/Apple's ## Why Use Cappuccino? -Cappuccino is not intended for building simple websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. +Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. Cappuccino is not intended for building simple websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: * **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Showcase application](https://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3). Also take a look at the [Cookbook tutorial](https://cappuccino-cookbook.5apps.com/). * **🚀 Incredible Productivity:** Less code is needed. High-level abstractions and a powerful object-oriented model mean development is focused on application logic, not browser quirks. From 9d24c855e27632e616612169d0d4b734242fa396 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 6 May 2026 19:55:15 +0200 Subject: [PATCH 08/39] Update project status in README Removed outdated information and clarified project status. --- README.markdown | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.markdown b/README.markdown index de4a02829..d82987db6 100644 --- a/README.markdown +++ b/README.markdown @@ -6,8 +6,6 @@ > **✨ Project Status: Active Development & Node.js Transition** > Cappuccino has been under continuous development since 2008 and is actively maintained. A major transition to a modern, **Node.js-based toolchain** has recently been finalized. The current release is a production-ready Release Candidate, with a formal release scheduled for 2026. It is stable, fast, and ready for new projects. ---- - ## Why Use Cappuccino? Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. Cappuccino is not intended for building simple websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: From 78e1ea43c81dd84f59b976dd86f13ac951ea920c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 15:58:16 +0200 Subject: [PATCH 09/39] new: layouting speedup in CPTextView --- AppKit/CPTextView/CPLayoutManager.j | 66 ++++++++++++++++++++++++++++- AppKit/CPTextView/CPTypesetter.j | 6 ++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index f64d8d6a4..103e19921 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -286,6 +286,8 @@ _oncontextmenuhandler = function () { return false; }; if (removeRange.length) _removeInvalidLineFragmentsRange = CPMakeRangeCopy(removeRange); + else + _removeInvalidLineFragmentsRange = nil; // We erased all lines if (!startIndex) @@ -716,7 +718,7 @@ _oncontextmenuhandler = function () { return false; }; var index = location - lineFragment._range.location; - return lineFragment._glyphsFrames[index]._descent; + return [lineFragment glyphFrames][index]._descent; } - (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect @@ -1074,6 +1076,7 @@ var _objectsInRange = function(aList, aRange) BOOL _isInvalid; BOOL _isLast; + BOOL _exactFramesCalculated; CGRect _fragmentRect; CGRect _usedRect; CGPoint _location; @@ -1161,6 +1164,7 @@ var _objectsInRange = function(aList, aRange) _range = CPMakeRangeCopy(aRange); _textContainer = aContainer; _isInvalid = NO; + _exactFramesCalculated = NO; _runs = []; _glyphsFrames = []; _glyphsOffsets = []; @@ -1207,6 +1211,8 @@ var _objectsInRange = function(aList, aRange) - (void)setAdvancements:(CPArray)someAdvancements { + _exactFramesCalculated = NO; + var count = someAdvancements.length, origin = CGPointMake(_fragmentRect.origin.x + _location.x, _fragmentRect.origin.y), height = _usedRect.size.height; @@ -1223,6 +1229,62 @@ var _objectsInRange = function(aList, aRange) } } +- (CPArray)glyphFrames +{ + if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) + { + _exactFramesCalculated = YES; + var originX = _fragmentRect.origin.x + _location.x, + currentX = originX, + frameIndex = 0, + l = _runs.length; + + // Wir gehen alle Runs (Wörter/Textblöcke in dieser Zeile) durch + for (var r = 0; r < l; r++) + { + var run = _runs[r], + runStr = run.string; + + if (!runStr) + { + // Attachments / Unsichtbares überspringen + if (frameIndex < _glyphsFrames.length) + { + var frame = _glyphsFrames[frameIndex]; + frame.origin.x = currentX; + currentX += frame.size.width; + frameIndex++; + } + continue; + } + + var runLen = runStr.length, + runFont = run.font; + + // Nun messen wir für diesen sichtbaren Run den exakten Substring inkl. Kerning/Ligaturen + for (var i = 0; i < runLen; i++) + { + if (frameIndex >= _glyphsFrames.length) break; + + var frame = _glyphsFrames[frameIndex], + prefix = runStr.substr(0, i), + prefixWidth = prefix.length > 0 ? [prefix sizeWithFont:runFont inWidth:NULL].width : 0.0, + prefixWithChar = runStr.substr(0, i + 1), + prefixWithCharWidth = [prefixWithChar sizeWithFont:runFont inWidth:NULL].width; + + frame.origin.x = currentX + prefixWidth; + frame.size.width = prefixWithCharWidth - prefixWidth; + + frameIndex++; + } + + currentX += [runStr sizeWithFont:runFont inWidth:NULL].width; + } + } + + return _glyphsFrames; +} + - (void)_adjustForHeight:(double)height { var count = _glyphsFrames.length; @@ -1281,6 +1343,8 @@ var _objectsInRange = function(aList, aRange) - (void)drawInContext:(CGContext)context atPoint:(CGPoint)aPoint forRange:(CPRange)aRange { + [self glyphFrames]; // Erzwingt die exakte Berechnung, bevor gezeichnet wird! + var runs = _objectsInRange(_runs, aRange), c = runs.length, orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y); diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 49169206f..e8946fcfc 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -316,8 +316,10 @@ var CPSystemTypesetterFactory, lineRange.length++; measuringRange.length++; - var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons - rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor; + var currentCharCode = theString.charCodeAt(glyphIndex), + charStr = theString.charAt(glyphIndex), + charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, + rangeWidth = prevRangeWidth + charWidth; switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char. { From d6c5d8b4f2bdc25bb670b0aaf32b33a0204d1bd1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 16:47:12 +0200 Subject: [PATCH 10/39] formatting --- AppKit/CPTextView/CPLayoutManager.j | 4 ++++ AppKit/CPTextView/CPTypesetter.j | 3 +++ 2 files changed, 7 insertions(+) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 103e19921..6179243b9 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -1229,6 +1229,10 @@ var _objectsInRange = function(aList, aRange) } } +// The spacings with kerning/ligatures are recalculated here for the visible part +// See comment in layoutGlyphsInLayoutManager: of CPSimpleTypesetter +// This gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) + - (CPArray)glyphFrames { if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index e8946fcfc..418bfbb4e 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -318,6 +318,9 @@ var CPSystemTypesetterFactory, var currentCharCode = theString.charCodeAt(glyphIndex), charStr = theString.charAt(glyphIndex), + // this is ignoring kerning and ligatures but 100x faster than calculating the full string + // we cut corners here and do the exact calculation in - glyphFrames of _CPLineFragment (CPLayoutManager.j) + // this gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, rangeWidth = prevRangeWidth + charWidth; From 7f25de0fad1a3dea879ad6d74dde4683960d6bb9 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 21:30:11 +0200 Subject: [PATCH 11/39] improved: kerning safe optimization --- AppKit/CPTextView/CPLayoutManager.j | 68 +---------------------------- AppKit/CPTextView/CPTypesetter.j | 20 ++++++--- 2 files changed, 14 insertions(+), 74 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 6179243b9..926e2b993 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -718,7 +718,7 @@ _oncontextmenuhandler = function () { return false; }; var index = location - lineFragment._range.location; - return [lineFragment glyphFrames][index]._descent; + return lineFragment._glyphsFrames[index]._descent; } - (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect @@ -1076,7 +1076,6 @@ var _objectsInRange = function(aList, aRange) BOOL _isInvalid; BOOL _isLast; - BOOL _exactFramesCalculated; CGRect _fragmentRect; CGRect _usedRect; CGPoint _location; @@ -1164,7 +1163,6 @@ var _objectsInRange = function(aList, aRange) _range = CPMakeRangeCopy(aRange); _textContainer = aContainer; _isInvalid = NO; - _exactFramesCalculated = NO; _runs = []; _glyphsFrames = []; _glyphsOffsets = []; @@ -1211,8 +1209,6 @@ var _objectsInRange = function(aList, aRange) - (void)setAdvancements:(CPArray)someAdvancements { - _exactFramesCalculated = NO; - var count = someAdvancements.length, origin = CGPointMake(_fragmentRect.origin.x + _location.x, _fragmentRect.origin.y), height = _usedRect.size.height; @@ -1229,66 +1225,6 @@ var _objectsInRange = function(aList, aRange) } } -// The spacings with kerning/ligatures are recalculated here for the visible part -// See comment in layoutGlyphsInLayoutManager: of CPSimpleTypesetter -// This gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) - -- (CPArray)glyphFrames -{ - if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) - { - _exactFramesCalculated = YES; - var originX = _fragmentRect.origin.x + _location.x, - currentX = originX, - frameIndex = 0, - l = _runs.length; - - // Wir gehen alle Runs (Wörter/Textblöcke in dieser Zeile) durch - for (var r = 0; r < l; r++) - { - var run = _runs[r], - runStr = run.string; - - if (!runStr) - { - // Attachments / Unsichtbares überspringen - if (frameIndex < _glyphsFrames.length) - { - var frame = _glyphsFrames[frameIndex]; - frame.origin.x = currentX; - currentX += frame.size.width; - frameIndex++; - } - continue; - } - - var runLen = runStr.length, - runFont = run.font; - - // Nun messen wir für diesen sichtbaren Run den exakten Substring inkl. Kerning/Ligaturen - for (var i = 0; i < runLen; i++) - { - if (frameIndex >= _glyphsFrames.length) break; - - var frame = _glyphsFrames[frameIndex], - prefix = runStr.substr(0, i), - prefixWidth = prefix.length > 0 ? [prefix sizeWithFont:runFont inWidth:NULL].width : 0.0, - prefixWithChar = runStr.substr(0, i + 1), - prefixWithCharWidth = [prefixWithChar sizeWithFont:runFont inWidth:NULL].width; - - frame.origin.x = currentX + prefixWidth; - frame.size.width = prefixWithCharWidth - prefixWidth; - - frameIndex++; - } - - currentX += [runStr sizeWithFont:runFont inWidth:NULL].width; - } - } - - return _glyphsFrames; -} - - (void)_adjustForHeight:(double)height { var count = _glyphsFrames.length; @@ -1347,8 +1283,6 @@ var _objectsInRange = function(aList, aRange) - (void)drawInContext:(CGContext)context atPoint:(CGPoint)aPoint forRange:(CPRange)aRange { - [self glyphFrames]; // Erzwingt die exakte Berechnung, bevor gezeichnet wird! - var runs = _objectsInRange(_runs, aRange), c = runs.length, orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y); diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 418bfbb4e..ef793cb86 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -316,13 +316,8 @@ var CPSystemTypesetterFactory, lineRange.length++; measuringRange.length++; - var currentCharCode = theString.charCodeAt(glyphIndex), - charStr = theString.charAt(glyphIndex), - // this is ignoring kerning and ligatures but 100x faster than calculating the full string - // we cut corners here and do the exact calculation in - glyphFrames of _CPLineFragment (CPLayoutManager.j) - // this gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) - charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, - rangeWidth = prevRangeWidth + charWidth; + var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons + rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor; switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char. { @@ -370,6 +365,17 @@ var CPSystemTypesetterFactory, wrapWidth = rangeWidth; wrapRange._height = _lineHeight; wrapRange._base = _lineBase; + + // Optimization: Start measuring from the next character to avoid O(n^2) + // string width calculation within a line since spaces do not carry ligatures or kerning. + // Only reset the measuring range if the next character is NOT another space. + // This prevents compounded subpixel rounding errors with contiguous spaces. + if (theString.charCodeAt(glyphIndex + 1) !== 32) + { + currentAnchor = rangeWidth; + measuringRange = CPMakeRange(glyphIndex + 1, 0); + } + break; case 10: From abe159ecebaa151a2ffa6e9b2bf6ba04b80d875b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 21 May 2026 09:44:54 +0200 Subject: [PATCH 12/39] CPLayoutManager: Fix O(N^2) layout bottleneck when loading large text documents --- AppKit/CPTextView/CPLayoutManager.j | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 926e2b993..fd0accf43 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -305,13 +305,17 @@ _oncontextmenuhandler = function () { return false; }; - (BOOL)_rescuingInvalidFragmentsWasPossibleForGlyphRange:(CPRange)aRange { - var l = _lineFragments.length, - location = aRange.location, - found = NO, - targetLine = 0; + // 1. EARLY EXIT: If there are no fragments to rescue (e.g. setting new text), do nothing. + if (!_lineFragmentsForRescue || _lineFragmentsForRescue.length === 0) + return NO; - // try to find the first linefragment of the desired range - for (; targetLine < l; targetLine++) + var l = _lineFragments.length, + location = aRange.location, + found = NO, + targetLine = l - 1; // Start from the END of the array + + // 2. REVERSE SEARCH: The fragment we want is almost always at the end. + for (; targetLine >= 0; targetLine--) { if (CPLocationInRange(location, _lineFragments[targetLine]._range)) { @@ -334,9 +338,6 @@ _oncontextmenuhandler = function () { return false; }; newLength = [[_textStorage string].length], removalSkip = 1; - // if (ABS(newLength - oldLength) > 1) - // return NO; - if (![oldLineFragment isVisuallyIdenticalToFragment:newLineFragment]) { isIdentical = NO; From c27b48d27a2abf56396aeab3dab88376b6519478 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 23 May 2026 08:46:29 +0200 Subject: [PATCH 13/39] Update documentation links in README --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index d82987db6..47c3c0b17 100644 --- a/README.markdown +++ b/README.markdown @@ -78,7 +78,7 @@ Pure JavaScript and Objective-J can be mixed and matched, even in the same file. ## Find Out More * **Official Website:** [cappuccino.dev](http://cappuccino.dev) -* **Documentation & Tutorials:** [cappuccino.dev/learn/](http://cappuccino.dev/learn/), [cappuccino cookbook](https://cappuccino-cookbook.5apps.com) +* **Documentation & Tutorials:** [cappuccino.dev/learn/](http://cappuccino.dev/learn/), [Browser online documentation](https://daboe01.github.io/CappDoc/), [cappuccino cookbook](https://cappuccino-cookbook.5apps.com) * **Gitter Community Chat:** [gitter.im/cappuccino/cappuccino](https://gitter.im/cappuccino/cappuccino) * **GitHub Wiki:** [github.com/cappuccino/cappuccino/wiki](https://github.com/cappuccino/cappuccino/wiki) * **FAQ:** [cappuccino.dev/support/faq.html](http://cappuccino.dev/support/faq.html) From 9312c9615495a480cd4d80a0714d062086d51832 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 23 May 2026 09:05:07 +0200 Subject: [PATCH 14/39] Clarify Cappuccino's purpose in README --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 47c3c0b17..f528fee34 100644 --- a/README.markdown +++ b/README.markdown @@ -8,7 +8,7 @@ ## Why Use Cappuccino? -Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. Cappuccino is not intended for building simple websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: +Cappuccino is not intended for building websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: * **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Showcase application](https://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3). Also take a look at the [Cookbook tutorial](https://cappuccino-cookbook.5apps.com/). * **🚀 Incredible Productivity:** Less code is needed. High-level abstractions and a powerful object-oriented model mean development is focused on application logic, not browser quirks. From 8258c8ef0be3e2291ecb22c90760b7c2f953b45f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 23 May 2026 13:51:25 +0200 Subject: [PATCH 15/39] Revise Cappuccino usage description in README --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index f528fee34..d261d22f6 100644 --- a/README.markdown +++ b/README.markdown @@ -8,7 +8,7 @@ ## Why Use Cappuccino? -Cappuccino is not intended for building websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: +Cappuccino is for building **applications** in the browser —especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: * **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Showcase application](https://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3). Also take a look at the [Cookbook tutorial](https://cappuccino-cookbook.5apps.com/). * **🚀 Incredible Productivity:** Less code is needed. High-level abstractions and a powerful object-oriented model mean development is focused on application logic, not browser quirks. From dcb06fbc4f7cd515c5ef7555cf672f25a9d43387 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 23 May 2026 13:51:50 +0200 Subject: [PATCH 16/39] Fix formatting issues in README.markdown --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index d261d22f6..300f255b6 100644 --- a/README.markdown +++ b/README.markdown @@ -8,7 +8,7 @@ ## Why Use Cappuccino? -Cappuccino is for building **applications** in the browser —especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: +Cappuccino is for building **applications** in the browser — especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: * **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Showcase application](https://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3). Also take a look at the [Cookbook tutorial](https://cappuccino-cookbook.5apps.com/). * **🚀 Incredible Productivity:** Less code is needed. High-level abstractions and a powerful object-oriented model mean development is focused on application logic, not browser quirks. From 6099311cbdb1c1e94f8e6e3ef6912726573024bb Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 25 May 2026 06:48:27 +0200 Subject: [PATCH 17/39] Update _CPRTFParser.j --- AppKit/CPTextView/_CPRTFParser.j | 65 ++++++++++++++------------------ 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 2fdb03985..1adac0311 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -40,6 +40,15 @@ FIXME: this class should be redone using a 'real' parser var hexTable = []; +var cp1252Map = { + 0x80: 0x20AC, 0x82: 0x201A, 0x83: 0x0192, 0x84: 0x201E, 0x85: 0x2026, + 0x86: 0x2020, 0x87: 0x2021, 0x88: 0x02C6, 0x89: 0x2030, 0x8A: 0x0160, + 0x8B: 0x2039, 0x8C: 0x0152, 0x8E: 0x017D, 0x91: 0x2018, 0x92: 0x2019, + 0x93: 0x201C, 0x94: 0x201D, 0x95: 0x2022, 0x96: 0x2013, 0x97: 0x2014, + 0x98: 0x02DC, 0x99: 0x2122, 0x9A: 0x0161, 0x9B: 0x203A, 0x9C: 0x0153, + 0x9E: 0x017E, 0x9F: 0x0178 +}; + // Hold the attributes of the current run @implementation _RTFAttribute : CPObject { @@ -346,25 +355,23 @@ var kRgsymRtf = { return ''; case "ipfnHex": - ch = _rtf.charAt(++_currentParseIndex); - var hex = ''; - - while (new RegExp("[a-fA-F0-9\\']").test(ch)) + // Konsumiere exakt 2 Zeichen nach dem \' + for (var i = 0; i < 2; i++) { - if (ch == "'") + var nextCh = _rtf.charAt(++_currentParseIndex); + if (/[a-fA-F0-9]/.test(nextCh)) { - _currentParseIndex++; - continue; + hex += nextCh; + } + else + { + _currentParseIndex--; + break; } - - hex += (ch + ''); - ch = _rtf.charAt(++_currentParseIndex); } - //ch = parseInt(ch, 16); - //console.log("hex : " + hex); + _hexreturn = YES; - _currentParseIndex--; if (_curState !== 0) return ''; @@ -734,32 +741,16 @@ var kRgsymRtf = { { if (ch.length > 0) { - if (parseInt(ch, 16) & 0x80) - { - hex += ch.toUpperCase(); - } - else - { - [self _appendPlainString: String.fromCharCode(parseInt((hex + ch), 16))]; - hex = ''; - } - - if (hex.length == 4) - { - var temp = parseInt(hex, 16); - - if (hexTable && hexTable[hex.toUpperCase()] !== undefined) - temp = parseInt(hexTable[hex.toUpperCase()], 16); - - [self _appendPlainString: String.fromCharCode(temp)]; - hex = ''; + var byteVal = parseInt(ch, 16); + var unicodeVal = byteVal; + + // Windows-1252 Mapping für den Bereich 0x80 - 0x9F anwenden + if (byteVal >= 0x80 && byteVal <= 0x9F) { + unicodeVal = cp1252Map[byteVal] || byteVal; } + + [self _appendPlainString: String.fromCharCode(unicodeVal)]; } - else - { - CPLogConsole("hex skipped"); - } - _hexreturn = NO; } else if (ch !== undefined && _curState === 0) From 141eb7b98ec27887bc215da650906ccad728192d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 25 May 2026 06:56:32 +0200 Subject: [PATCH 18/39] fixed: wrong characters in output --- AppKit/CPTextView/_CPRTFParser.j | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 1adac0311..0530d5403 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -330,17 +330,16 @@ var kRgsymRtf = { - (BOOL)pushState { - _states.push["group"]; + _states.push(_curState); return YES; } - (BOOL)popState { - _states.pop(); - - if (_curState > 0) - _curState--; - + if (_states.length > 0) + { + _curState = _states.pop(); + } return YES; } @@ -603,9 +602,6 @@ var kRgsymRtf = { } - if (_states.length > 0) - _curState = 1; - return ''; } } From 6e36fc0b2066bcba7fe3921e7429002a1c32848b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 17:51:10 +0200 Subject: [PATCH 19/39] formatting --- AppKit/CPTextView/CPRulerView.j | 1 + 1 file changed, 1 insertion(+) diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index 008d212bf..1f9a9a311 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -16,6 +16,7 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + @import @import @import From 7e0f03a6fc8d1ee820b7a925acb7e4e253870ddc Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 21:03:31 +0200 Subject: [PATCH 20/39] fixed: CI error --- AppKit/CPTextView/CPRulerView.j | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index 1f9a9a311..102bba5ef 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -17,14 +17,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -@import -@import -@import -@import -@import -@import -@import -@import +@import "CPView.j" +@import "CPScrollView.j" +@import "CPBezierPath.j" +@import "CPColor.j" +@import "CPStringDrawing.j" +@import "CPFont.j" +@import "CPImage.j" @class CPRulerMarker From 281954671682abbf31ad336449c8cf9a1067800c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 21:07:43 +0200 Subject: [PATCH 21/39] fixed: missing import --- Tests/AppKit/CPTextViewTest.j | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/AppKit/CPTextViewTest.j b/Tests/AppKit/CPTextViewTest.j index f198a5ed9..a79485e41 100644 --- a/Tests/AppKit/CPTextViewTest.j +++ b/Tests/AppKit/CPTextViewTest.j @@ -1,5 +1,6 @@ @import @import +@import @import @implementation CPTextViewTest : OJTestCase From 3aecb3ea988177f105979f746e6e90e92ea5b79e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 21:11:51 +0200 Subject: [PATCH 22/39] fixed: missing import --- AppKit/CPTextView/CPTextView.j | 1 + Tests/AppKit/CPTextViewTest.j | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index f51a0da11..501c967b2 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -31,6 +31,7 @@ @import "CPTextStorage.j" @import "CPTextContainer.j" @import "CPLayoutManager.j" +@import "CPParagraphStyle.j" @import "_CPRTFParser.j" @import "_CPRTFProducer.j" diff --git a/Tests/AppKit/CPTextViewTest.j b/Tests/AppKit/CPTextViewTest.j index a79485e41..f198a5ed9 100644 --- a/Tests/AppKit/CPTextViewTest.j +++ b/Tests/AppKit/CPTextViewTest.j @@ -1,6 +1,5 @@ @import @import -@import @import @implementation CPTextViewTest : OJTestCase From c6549867bdd75784860c04efcd6ebb6dcfbf6acc Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 21:13:42 +0200 Subject: [PATCH 23/39] fixed: missing global variable --- AppKit/CPTextView/CPParagraphStyle.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPTextView/CPParagraphStyle.j b/AppKit/CPTextView/CPParagraphStyle.j index c6d2c1b71..3ad86f0ce 100644 --- a/AppKit/CPTextView/CPParagraphStyle.j +++ b/AppKit/CPTextView/CPParagraphStyle.j @@ -24,7 +24,7 @@ @import @import -var CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName"; +CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName"; // Standard Tab Interval (28pts is roughly 4 spaces in standard fonts) var kDefaultTabInterval = 28.0; From 420f3a538d3e387804077f00bd17764a5c197dc9 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 21:23:01 +0200 Subject: [PATCH 24/39] Update AppController.j --- Tests/Manual/CPTextView/AppController.j | 291 +++++++++++++++++++----- 1 file changed, 229 insertions(+), 62 deletions(-) diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 750a34d00..0d629ce75 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -8,33 +8,41 @@ @import @import @import +@import +@import +@import +@import +@import +@import @implementation AppController : CPObject { - CPTextView _textView; - CPTextView _textView2; + CPTextView _textView; + CPTextView _textView2; + CPScrollView _scrollView; + CPScrollView _scrollView2; } - - -- (void) openSheet:(id)sender +- (void)openSheet:(id)sender { - var plusPopover =[CPPopover new]; + var plusPopover = [CPPopover new]; [plusPopover setDelegate:self]; [plusPopover setAnimates:NO]; [plusPopover setBehavior:CPPopoverBehaviorTransient]; [plusPopover setAppearance:CPPopoverAppearanceMinimal]; - var myViewController=[CPViewController new]; + + var myViewController = [CPViewController new]; [plusPopover setContentViewController:myViewController]; + var textView = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, 200, 10)]; [textView setBackgroundColor:[CPColor whiteColor]]; + var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, 200, 150)]; - [scrollView setDocumentView:textView]; + [myViewController setView:scrollView]; [plusPopover showRelativeToRect:NULL ofView:sender preferredEdge:nil]; [[textView window] makeFirstResponder:textView]; - } - (void)orderFrontFontPanel:(id)sender @@ -42,89 +50,248 @@ [[CPFontManager sharedFontManager] orderFrontFontPanel:self]; } +- (void)toggleRuler:(id)sender +{ + [_scrollView setRulersVisible:![_scrollView rulersVisible]]; +} + +- (void)alignLeft:(id)sender +{ + [_textView alignLeft:self]; +} + +- (void)alignCenter:(id)sender +{ + [_textView alignCenter:self]; +} + +- (void)alignRight:(id)sender +{ + [_textView alignRight:self]; +} + +- (void)alignJustified:(id)sender +{ + [_textView alignJustified:self]; +} + - (void)applicationDidFinishLaunching:(CPNotification)aNotification { - // CPLogRegister(CPLogConsole); - var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask], contentView = [theWindow contentView]; [contentView setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]]; - var mybutton=[[CPButton alloc] initWithFrame:CGRectMake(0, 0, 250, 25)]; - [mybutton setTitle:"Open sheet (must not be triggered by return)"] - [mybutton setTarget:self]; - [mybutton setAction:@selector(openSheet:)]; - [mybutton setKeyEquivalent:@"\r"]; + // 1. Clean visual header / toolbar area + var toolbarView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([contentView bounds]), 60)]; + [toolbarView setAutoresizingMask:CPViewWidthSizable]; + [toolbarView setBackgroundColor:[CPColor colorWithWhite:0.88 alpha:1.0]]; + [contentView addSubview:toolbarView]; - [contentView addSubview:mybutton]; + var currentX = 15; + // Popover Trigger + var sheetButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 145, 30)]; + [sheetButton setTitle:@"Open Popover Sheet"]; + [sheetButton setTarget:self]; + [sheetButton setAction:@selector(openSheet:)]; + [toolbarView addSubview:sheetButton]; + currentX += 155; - _textView = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, 500, 200)]; + // Font Panel Trigger + var fontButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 125, 30)]; + [fontButton setTitle:@"Show Font Panel"]; + [fontButton setTarget:self]; + [fontButton setAction:@selector(orderFrontFontPanel:)]; + [toolbarView addSubview:fontButton]; + currentX += 135; + + // Toggle Ruler Trigger + var rulerButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 120, 30)]; + [rulerButton setTitle:@"Toggle Ruler"]; + [rulerButton setTarget:self]; + [rulerButton setAction:@selector(toggleRuler:)]; + [toolbarView addSubview:rulerButton]; + currentX += 140; + + // RTF Roundtrip Trigger + var rtfButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 150, 30)]; + [rtfButton setTitle:@"RTF Round-trip ➔"]; + [rtfButton setTarget:self]; + [rtfButton setAction:@selector(makeRTF:)]; + [toolbarView addSubview:rtfButton]; + currentX += 165; + + // Text Alignment Group + var labelAlign = [[CPTextField alloc] initWithFrame:CGRectMake(currentX, 22, 45, 20)]; + [labelAlign setStringValue:@"Align:"]; + [labelAlign setFont:[CPFont systemFontOfSize:12]]; + [toolbarView addSubview:labelAlign]; + currentX += 45; + + var alignLeftBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 30, 30)]; + [alignLeftBtn setTitle:@"⃔"]; // Left-align symbol + [alignLeftBtn setTarget:self]; + [alignLeftBtn setAction:@selector(alignLeft:)]; + [toolbarView addSubview:alignLeftBtn]; + currentX += 32; + + var alignCenterBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 30, 30)]; + [alignCenterBtn setTitle:@"↔"]; // Center-align symbol + [alignCenterBtn setTarget:self]; + [alignCenterBtn setAction:@selector(alignCenter:)]; + [toolbarView addSubview:alignCenterBtn]; + currentX += 32; + + var alignRightBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 30, 30)]; + [alignRightBtn setTitle:@"⃕"]; // Right-align symbol + [alignRightBtn setTarget:self]; + [alignRightBtn setAction:@selector(alignRight:)]; + [toolbarView addSubview:alignRightBtn]; + currentX += 32; + + var alignJustifyBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 30, 30)]; + [alignJustifyBtn setTitle:@"≡"]; // Justify-align symbol + [alignJustifyBtn setTarget:self]; + [alignJustifyBtn setAction:@selector(alignJustified:)]; + [toolbarView addSubview:alignJustifyBtn]; + + // Default return key target test (as defined in original source) + var returnButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth([contentView bounds]) - 270, 15, 250, 30)]; + [returnButton setAutoresizingMask:CPViewMinXMargin]; + [returnButton setTitle:@"Key Return Target"]; + [returnButton setTarget:self]; + [returnButton setAction:@selector(openSheet:)]; + [returnButton setKeyEquivalent:@"\r"]; + [toolbarView addSubview:returnButton]; + + // 2. Main content area: Split View Layout for side-by-side comparison + var splitView = [[CPSplitView alloc] initWithFrame:CGRectMake(0, 60, CGRectGetWidth([contentView bounds]), CGRectGetHeight([contentView bounds]) - 60)]; + [splitView setVertical:YES]; + [splitView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + + var leftContainer = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([splitView bounds]) / 2, CGRectGetHeight([splitView bounds]))]; + [leftContainer setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + + var rightContainer = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([splitView bounds]) / 2, CGRectGetHeight([splitView bounds]))]; + [rightContainer setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + + [splitView addSubview:leftContainer]; + [splitView addSubview:rightContainer]; + [contentView addSubview:splitView]; + + // Left Container: label & Scroll/TextView containing Ruler + var leftLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 10, CGRectGetWidth([leftContainer bounds]) - 30, 20)]; + [leftLabel setStringValue:@"Rich Text Editor"]; + [leftLabel setFont:[CPFont boldSystemFontOfSize:14]]; + [leftLabel setAutoresizingMask:CPViewWidthSizable]; + [leftContainer addSubview:leftLabel]; + + _textView = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([leftContainer bounds]) - 30, CGRectGetHeight([leftContainer bounds]) - 70)]; [_textView setRichText:YES]; - - _textView2 = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, 1000, 200)]; - _textView2._isRichText = NO; [_textView setBackgroundColor:[CPColor whiteColor]]; + + _scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(15, 40, CGRectGetWidth([leftContainer bounds]) - 30, CGRectGetHeight([leftContainer bounds]) - 65)]; + [_scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_scrollView setDocumentView:_textView]; + + // ATTACH THE NEW CPRULERVIEW SYSTEM + [_scrollView setHasHorizontalRuler:YES]; + [_scrollView setRulersVisible:YES]; + [leftContainer addSubview:_scrollView]; + + // Right Container: RTF Plain-Text Source and Parser Window + var rightLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 10, CGRectGetWidth([rightContainer bounds]) - 30, 20)]; + [rightLabel setStringValue:@"RTF Raw Output & Source Parser Window"]; + [rightLabel setFont:[CPFont boldSystemFontOfSize:14]]; + [rightLabel setAutoresizingMask:CPViewWidthSizable]; + [rightContainer addSubview:rightLabel]; + + _textView2 = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([rightContainer bounds]) - 30, CGRectGetHeight([rightContainer bounds]) - 70)]; + _textView2._isRichText = NO; [_textView2 setBackgroundColor:[CPColor whiteColor]]; - var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 70, 520, 220)]; - var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 550, 1020, 220)]; + _scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(15, 40, CGRectGetWidth([rightContainer bounds]) - 30, CGRectGetHeight([rightContainer bounds]) - 65)]; + [_scrollView2 setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_scrollView2 setDocumentView:_textView2]; + [rightContainer addSubview:_scrollView2]; - [scrollView setDocumentView:_textView]; - [scrollView2 setDocumentView:_textView2]; + // 3. Build application Main Menu + var mainMenu = [CPApp mainMenu]; - [contentView addSubview: scrollView]; - [contentView addSubview: scrollView2]; + while ([mainMenu numberOfItems] > 0) + [mainMenu removeItemAtIndex:0]; - // build our menu - var mainMenu = [CPApp mainMenu]; + var item = [mainMenu insertItemWithTitle:@"Edit" action:nil keyEquivalent:nil atIndex:0], + editMenu = [[CPMenu alloc] initWithTitle:@"Edit Menu"]; - while ([mainMenu numberOfItems] > 0) - [mainMenu removeItemAtIndex:0]; + [editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"]; + [editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; + [editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; + [editMenu addItemWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""]; + [editMenu addItemWithTitle:@"Select All" action:@selector(selectAll:) keyEquivalent:@"a"]; + [editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"]; + [editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"]; + [mainMenu setSubmenu:editMenu forItem:item]; - var item = [mainMenu insertItemWithTitle:@"Edit" action:nil keyEquivalent:nil atIndex:0], - editMenu = [[CPMenu alloc] initWithTitle:@"Edit Menu"]; + item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0]; + var formatMenu = [[CPMenu alloc] initWithTitle:@"Format Menu"]; + [formatMenu addItemWithTitle:@"Font panel" action:@selector(orderFrontFontPanel:) keyEquivalent:@"f"]; + [formatMenu addItemWithTitle:@"Underline" action:@selector(underline:) keyEquivalent:@"u"]; + [formatMenu addItemWithTitle:@"Align Left" action:@selector(alignLeft:) keyEquivalent:@"{"]; + [formatMenu addItemWithTitle:@"Align Center" action:@selector(alignCenter:) keyEquivalent:@"|"]; + [formatMenu addItemWithTitle:@"Align Right" action:@selector(alignRight:) keyEquivalent:@"}"]; + [mainMenu setSubmenu:formatMenu forItem:item]; - [editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"]; - [editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; - [editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; - [editMenu addItemWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""]; - [editMenu addItemWithTitle:@"Select All" action:@selector(selectAll:) keyEquivalent:@"a"]; - [editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"]; - [editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"]; - - [mainMenu setSubmenu:editMenu forItem:item]; - - item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0]; - var formatMenu = [[CPMenu alloc] initWithTitle:@"Format Menu"]; - [formatMenu addItemWithTitle:@"Font panel" action:@selector(orderFrontFontPanel:) keyEquivalent:@"f"]; - [formatMenu addItemWithTitle:@"Underline" action:@selector(underline:) keyEquivalent:@"u"]; - [mainMenu setSubmenu:formatMenu forItem:item]; - - [_textView insertText:"123"]; + // 4. Load Rich Sample Text content + [_textView insertText:@"123"]; var tempImageView = [[CPImageView alloc] initWithFrame:CGRectMake(0, 0, 32, 32)]; - [tempImageView setImage:[[CPImage alloc] initWithContentsOfFile:@"Resources/spinner.gif" size:CGSizeMake(32, 32)]] + [tempImageView setImage:[[CPImage alloc] initWithContentsOfFile:@"Resources/spinner.gif" size:CGSizeMake(32, 32)]]; [_textView insertText:[CPTextStorage attributedStringWithAttachment:tempImageView]]; - [_textView insertText:" 456 "]; + [_textView insertText:@" 456 "]; - var tempButton = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 64, 28)] + var tempButton = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 64, 28)]; [_textView insertText:[CPTextStorage attributedStringWithAttachment:tempButton]]; - var centeredParagraph=[CPParagraphStyle new]; - [centeredParagraph setAlignment: CPCenterTextAlignment]; - [_textView insertText:"\n"]; + // Centered paragraph text block + var centeredParagraph = [CPParagraphStyle new]; + [centeredParagraph setAlignment:CPCenterTextAlignment]; + [_textView insertText:@"\n"]; [_textView insertText:[[CPAttributedString alloc] initWithString:@"Fusce\n" - attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:"Arial" size:18], [CPColor redColor], [CPColor yellowColor]] + attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:@"Arial" size:18], [CPColor redColor], [CPColor yellowColor]] forKeys:[CPParagraphStyleAttributeName, CPFontAttributeName, CPForegroundColorAttributeName, CPBackgroundColorAttributeName]]]]; - [_textView insertText:"\n"]; - [_textView insertText:[[CPAttributedString alloc] initWithString:@"Yellow\n" - attributes:[CPDictionary dictionaryWithObjects:[[CPFont boldFontWithName:"Arial" size:25], [CPColor yellowColor]] + // Highlighted Heading + [_textView insertText:@"\n"]; + [_textView insertText:[[CPAttributedString alloc] initWithString:@"Interactive Ruler Showcase\n" + attributes:[CPDictionary dictionaryWithObjects:[[CPFont boldFontWithName:@"Arial" size:22], [CPColor yellowColor]] forKeys:[CPFontAttributeName, CPBackgroundColorAttributeName]]]]; + + // DEMONSTRATION OF CPRULERVIEW TAB MARKERS + // Creating left, center, and right tabs to showcase the ruler markers dynamically + var tabParagraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; + var tab1 = [[CPTextTab alloc] initWithType:CPLeftTabStopType location:100.0]; + var tab2 = [[CPTextTab alloc] initWithType:CPCenterTabStopType location:220.0]; + var tab3 = [[CPTextTab alloc] initWithType:CPRightTabStopType location:340.0]; + [tabParagraph setTabStops:[tab1, tab2, tab3]]; + + [_textView insertText:@"\n"]; + [_textView insertText:[[CPAttributedString alloc] initWithString:@"Tab1\tTab2\tTab3\nLeftAlign\tCenterAlign\tRightAlign\n" + attributes:[CPDictionary dictionaryWithObject:tabParagraph forKey:CPParagraphStyleAttributeName]]]; + + // DEMONSTRATION OF INDENTATION MARKERS + // Creating custom margin and indentation settings + var indentParagraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; + [indentParagraph setFirstLineHeadIndent:30.0]; + [indentParagraph setHeadIndent:50.0]; + [indentParagraph setTailIndent:-30.0]; + + [_textView insertText:@"\n"]; + [_textView insertText:[[CPAttributedString alloc] initWithString:@"This paragraph has a first-line indent of 30pt, a head indent of 50pt, and a tail indent of -30pt. Check the horizontal ruler above to see how the indent markers align with this paragraph, and adjust them directly!\n" + attributes:[CPDictionary dictionaryWithObject:indentParagraph forKey:CPParagraphStyleAttributeName]]]; + [theWindow orderFront:self]; -// [_textView setEditable:NO]; [CPMenu setMenuBarVisible:YES]; console.log([[CPFont systemFontOfSize:12] cssString]) @@ -135,7 +302,7 @@ console.log(ROUND([CPPlatformString sizeOfString:testingText withFont:[CPFont systemFontOfSize:12] forWidth:NULL].width)); } -- (void) makeRTF:(id)sender +- (void)makeRTF:(id)sender { [_textView2 setString: [_CPRTFProducer produceRTF:[_textView textStorage] documentAttributes: @{}] ]; var tc = [_CPRTFParser new]; From 1d93cf2b1a077f3d9ce3d2012c4b5c4067f113fc Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 21:31:26 +0200 Subject: [PATCH 25/39] new: cpscrollview fixes --- AppKit/CPScrollView.j | 203 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 196 insertions(+), 7 deletions(-) diff --git a/AppKit/CPScrollView.j b/AppKit/CPScrollView.j index ac042e3a2..269b73880 100644 --- a/AppKit/CPScrollView.j +++ b/AppKit/CPScrollView.j @@ -31,6 +31,7 @@ @import "CPView.j" @class CPTableView +@class CPRulerView #define SHOULD_SHOW_CORNER_VIEW() (_scrollerStyle === CPScrollerStyleLegacy && _verticalScroller && ![_verticalScroller isHidden]) @@ -140,6 +141,14 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay, int _scrollerStyle; int _scrollerKnobStyle; + + // Ruler Support + BOOL _hasVerticalRuler; + BOOL _hasHorizontalRuler; + BOOL _rulersVisible; + + CPRulerView _verticalRuler; + CPRulerView _horizontalRuler; } @@ -306,6 +315,10 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay, _scrollerKnobStyle = CPScrollerKnobStyleDefault; [self setScrollerStyle:CPScrollerStyleGlobal]; + _hasVerticalRuler = NO; + _hasHorizontalRuler = NO; + _rulersVisible = NO; + _delegate = nil; _scrollTimer = nil; _implementedDelegateMethods = 0; @@ -794,6 +807,103 @@ Notifies the delegate when the scroll view has finished scrolling. } +#pragma mark - +#pragma mark Rulers + +- (BOOL)hasHorizontalRuler +{ + return _hasHorizontalRuler; +} + +- (void)setHasHorizontalRuler:(BOOL)shouldHaveHorizontalRuler +{ + if (_hasHorizontalRuler === shouldHaveHorizontalRuler) + return; + + _hasHorizontalRuler = shouldHaveHorizontalRuler; + + if (_hasHorizontalRuler && !_horizontalRuler) + { + _horizontalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPHorizontalRuler]; + } + + [self tile]; +} + +- (BOOL)hasVerticalRuler +{ + return _hasVerticalRuler; +} + +- (void)setHasVerticalRuler:(BOOL)shouldHaveVerticalRuler +{ + if (_hasVerticalRuler === shouldHaveVerticalRuler) + return; + + _hasVerticalRuler = shouldHaveVerticalRuler; + + if (_hasVerticalRuler && !_verticalRuler) + { + _verticalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPVerticalRuler]; + } + + [self tile]; +} + +- (BOOL)rulersVisible +{ + return _rulersVisible; +} + +- (void)setRulersVisible:(BOOL)areRulersVisible +{ + if (_rulersVisible === areRulersVisible) + return; + + _rulersVisible = areRulersVisible; + + [self tile]; +} + +- (CPRulerView)horizontalRulerView +{ + return _horizontalRuler; +} + +- (void)setHorizontalRulerView:(CPRulerView)aRulerView +{ + if (_horizontalRuler === aRulerView) + return; + + [_horizontalRuler removeFromSuperview]; + _horizontalRuler = aRulerView; + + if (_horizontalRuler) + [self addSubview:_horizontalRuler]; + + [self tile]; +} + +- (CPRulerView)verticalRulerView +{ + return _verticalRuler; +} + +- (void)setVerticalRulerView:(CPRulerView)aRulerView +{ + if (_verticalRuler === aRulerView) + return; + + [_verticalRuler removeFromSuperview]; + _verticalRuler = aRulerView; + + if (_verticalRuler) + [self addSubview:_verticalRuler]; + + [self tile]; +} + + #pragma mark - #pragma mark Privates @@ -1123,10 +1233,7 @@ Notifies the delegate when the scroll view has finished scrolling. */ - (void)tile { - // yuck. - // RESIZE: tile->setHidden AND refl - // Outside Change: refl->tile->setHidden AND refl - // scroll: refl. + [self reflectScrolledClipView:_contentView]; } /*! @@ -1170,6 +1277,41 @@ Notifies the delegate when the scroll view has finished scrolling. contentFrame.origin.y += headerClipViewHeight; contentFrame.size.height -= headerClipViewHeight; + // Adjust content view based on horizontal / vertical ruler presence + var showHorizontalRuler = _rulersVisible && _hasHorizontalRuler && _horizontalRuler, + showVerticalRuler = _rulersVisible && _hasVerticalRuler && _verticalRuler; + + var horizRulerThickness = showHorizontalRuler ? ([_horizontalRuler respondsToSelector:@selector(ruleThickness)] ? [_horizontalRuler ruleThickness] : 16.0) : 0.0, + vertRulerThickness = showVerticalRuler ? ([_verticalRuler respondsToSelector:@selector(ruleThickness)] ? [_verticalRuler ruleThickness] : 24.0) : 0.0; + + if (showHorizontalRuler) + { + if ([_horizontalRuler superview] !== self) + [self addSubview:_horizontalRuler]; + [_horizontalRuler setHidden:NO]; + } + else if (_horizontalRuler) + { + [_horizontalRuler setHidden:YES]; + } + + if (showVerticalRuler) + { + if ([_verticalRuler superview] !== self) + [self addSubview:_verticalRuler]; + [_verticalRuler setHidden:NO]; + } + else if (_verticalRuler) + { + [_verticalRuler setHidden:YES]; + } + + contentFrame.origin.y += horizRulerThickness; + contentFrame.size.height -= horizRulerThickness; + + contentFrame.origin.x += vertRulerThickness; + contentFrame.size.width -= vertRulerThickness; + var difference = CGSizeMake(CGRectGetWidth(documentFrame) - CGRectGetWidth(contentFrame), CGRectGetHeight(documentFrame) - CGRectGetHeight(contentFrame)), verticalScrollerWidth = [_verticalScroller scrollerWidth], horizontalScrollerHeight = [_horizontalScroller scrollerWidth], @@ -1262,6 +1404,7 @@ Notifies the delegate when the scroll view has finished scrolling. [_contentView setFrame:contentFrame]; [_headerClipView setFrame:[self _headerClipViewFrame]]; [[_headerClipView documentView] setNeedsDisplay:YES]; + if (SHOULD_SHOW_CORNER_VIEW()) { [_cornerView setFrame:[self _cornerViewFrame]]; @@ -1276,6 +1419,29 @@ Notifies the delegate when the scroll view has finished scrolling. [[self bottomCornerView] setBackgroundColor:[self currentValueForThemeAttribute:@"bottom-corner-color"]]; } + // Position and redraw rulers to track viewport updates + if (showHorizontalRuler) + { + [_horizontalRuler setFrame:CGRectMake( + CGRectGetMinX(contentFrame), + CGRectGetMinY(contentFrame) - horizRulerThickness, + CGRectGetWidth(contentFrame), + horizRulerThickness + )]; + [_horizontalRuler setNeedsDisplay:YES]; + } + + if (showVerticalRuler) + { + [_verticalRuler setFrame:CGRectMake( + CGRectGetMinX(contentFrame) - vertRulerThickness, + CGRectGetMinY(contentFrame), + vertRulerThickness, + CGRectGetHeight(contentFrame) + )]; + [_verticalRuler setNeedsDisplay:YES]; + } + --_recursionCount; } @@ -1438,8 +1604,8 @@ Notifies the delegate when the scroll view has finished scrolling. y = maxY - 1.5; - CGContextMoveToPoint(context, maxX - 1.0, y); - CGContextAddLineToPoint(context, minX + 2.0, y); + CGContextMoveToPoint(maxX - 1.0, y); + CGContextAddLineToPoint(minX + 2.0, y); x = minX + 0.5; @@ -1618,7 +1784,14 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView", CPScrollViewBottomCornerViewKey = @"CPScrollViewBottomCornerViewKey", CPScrollViewBorderTypeKey = @"CPScrollViewBorderTypeKey", CPScrollViewScrollerStyleKey = @"CPScrollViewScrollerStyleKey", - CPScrollViewScrollerKnobStyleKey = @"CPScrollViewScrollerKnobStyleKey"; + CPScrollViewScrollerKnobStyleKey = @"CPScrollViewScrollerKnobStyleKey", + + // Ruler Coding Keys + CPScrollViewHasVRulerKey = @"CPScrollViewHasVRuler", + CPScrollViewHasHRulerKey = @"CPScrollViewHasHRuler", + CPScrollViewRulersVisibleKey = @"CPScrollViewRulersVisible", + CPScrollViewVRulerKey = @"CPScrollViewVRuler", + CPScrollViewHRulerKey = @"CPScrollViewHRuler"; @implementation CPScrollView (CPCoding) @@ -1653,6 +1826,14 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView", _cornerView = [aCoder decodeObjectForKey:CPScrollViewCornerViewKey]; _bottomCornerView = [aCoder decodeObjectForKey:CPScrollViewBottomCornerViewKey]; + // Ruler decoding + _hasVerticalRuler = [aCoder decodeBoolForKey:CPScrollViewHasVRulerKey]; + _hasHorizontalRuler = [aCoder decodeBoolForKey:CPScrollViewHasHRulerKey]; + _rulersVisible = [aCoder decodeBoolForKey:CPScrollViewRulersVisibleKey]; + + _verticalRuler = [aCoder decodeObjectForKey:CPScrollViewVRulerKey]; + _horizontalRuler = [aCoder decodeObjectForKey:CPScrollViewHRulerKey]; + _delegate = nil; _scrollTimer = nil; _implementedDelegateMethods = 0; @@ -1706,6 +1887,14 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView", [aCoder encodeInt:_scrollerStyle forKey:CPScrollViewScrollerStyleKey]; [aCoder encodeInt:_scrollerKnobStyle forKey:CPScrollViewScrollerKnobStyleKey]; + + // Ruler encoding + [aCoder encodeBool:_hasVerticalRuler forKey:CPScrollViewHasVRulerKey]; + [aCoder encodeBool:_hasHorizontalRuler forKey:CPScrollViewHasHRulerKey]; + [aCoder encodeBool:_rulersVisible forKey:CPScrollViewRulersVisibleKey]; + + [aCoder encodeObject:_verticalRuler forKey:CPScrollViewVRulerKey]; + [aCoder encodeObject:_horizontalRuler forKey:CPScrollViewHRulerKey]; } @end From c94ed00f41a57a473e11d2d8253373ba756d05c0 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 29 May 2026 10:46:10 +0200 Subject: [PATCH 26/39] fixed: basic ruler view support --- AppKit/CPScrollView.j | 17 +- AppKit/CPTextView/CPParagraphStyle.j | 172 +-- AppKit/CPTextView/CPRulerView.j | 1297 +++++------------------ AppKit/CPTextView/CPTextView.j | 235 +++- AppKit/CPTextView/CPTypesetter.j | 62 +- Tests/Manual/CPTextView/AppController.j | 92 +- 6 files changed, 742 insertions(+), 1133 deletions(-) diff --git a/AppKit/CPScrollView.j b/AppKit/CPScrollView.j index 269b73880..444aee022 100644 --- a/AppKit/CPScrollView.j +++ b/AppKit/CPScrollView.j @@ -29,6 +29,7 @@ @import "CPClipView.j" @import "CPScroller.j" @import "CPView.j" +@import "CPRulerView.j" @class CPTableView @class CPRulerView @@ -824,7 +825,7 @@ Notifies the delegate when the scroll view has finished scrolling. if (_hasHorizontalRuler && !_horizontalRuler) { - _horizontalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPHorizontalRuler]; + _horizontalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPRulerOrientationHorizontal]; } [self tile]; @@ -844,7 +845,7 @@ Notifies the delegate when the scroll view has finished scrolling. if (_hasVerticalRuler && !_verticalRuler) { - _verticalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPVerticalRuler]; + _verticalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPRulerOrientationVertical]; } [self tile]; @@ -1428,7 +1429,11 @@ Notifies the delegate when the scroll view has finished scrolling. CGRectGetWidth(contentFrame), horizRulerThickness )]; - [_horizontalRuler setNeedsDisplay:YES]; + + if ([_horizontalRuler respondsToSelector:@selector(updateRuler)]) + [_horizontalRuler updateRuler]; + else + [_horizontalRuler setNeedsDisplay:YES]; } if (showVerticalRuler) @@ -1439,7 +1444,11 @@ Notifies the delegate when the scroll view has finished scrolling. vertRulerThickness, CGRectGetHeight(contentFrame) )]; - [_verticalRuler setNeedsDisplay:YES]; + + if ([_verticalRuler respondsToSelector:@selector(updateRuler)]) + [_verticalRuler updateRuler]; + else + [_verticalRuler setNeedsDisplay:YES]; } --_recursionCount; diff --git a/AppKit/CPTextView/CPParagraphStyle.j b/AppKit/CPTextView/CPParagraphStyle.j index 3ad86f0ce..2e2f3f6b9 100644 --- a/AppKit/CPTextView/CPParagraphStyle.j +++ b/AppKit/CPTextView/CPParagraphStyle.j @@ -217,24 +217,112 @@ var _sharedDefaultParagraphStyle = nil; @end +// MARK: - CPMutableParagraphStyle Implementation + // MARK: - CPMutableParagraphStyle Implementation @implementation CPMutableParagraphStyle : CPParagraphStyle { } +- (id)initWithParagraphStyle:(CPParagraphStyle)other +{ + if (self = [super initWithParagraphStyle:other]) + { + // Ensure our tab stops array is mutable in the mutable subclass + _tabStops = [[other tabStops] mutableCopy]; + } + return self; +} + +- (id)initWithCoder:(CPCoder)aCoder +{ + if (self = [super initWithCoder:aCoder]) + { + _tabStops = [_tabStops mutableCopy]; + } + return self; +} + +- (void)setLineSpacing:(float)aLineSpacing +{ + _lineSpacing = aLineSpacing; +} + +- (void)setParagraphSpacing:(float)aParagraphSpacing +{ + _paragraphSpacing = aParagraphSpacing; +} + +- (void)setAlignment:(CPTextAlignment)anAlignment +{ + _alignment = anAlignment; +} + +- (void)setHeadIndent:(float)aHeadIndent +{ + _headIndent = aHeadIndent; +} + +- (void)setTailIndent:(float)aTailIndent +{ + _tailIndent = aTailIndent; +} + +- (void)setFirstLineHeadIndent:(float)aFirstLineHeadIndent +{ + _firstLineHeadIndent = aFirstLineHeadIndent; +} + +- (void)setMinimumLineHeight:(float)aMinimumLineHeight +{ + _minimumLineHeight = aMinimumLineHeight; +} + +- (void)setMaximumLineHeight:(float)aMaximumLineHeight +{ + _maximumLineHeight = aMaximumLineHeight; +} + +- (void)setLineBreakMode:(CPLineBreakMode)aLineBreakMode +{ + _lineBreakMode = aLineBreakMode; +} + +- (void)setBaseWritingDirection:(CPWritingDirection)aBaseWritingDirection +{ + _baseWritingDirection = aBaseWritingDirection; +} + +- (void)setLineHeightMultiple:(float)aLineHeightMultiple +{ + _lineHeightMultiple = aLineHeightMultiple; +} + +- (void)setParagraphSpacingBefore:(float)aParagraphSpacingBefore +{ + _paragraphSpacingBefore = aParagraphSpacingBefore; +} + +- (void)setDefaultTabInterval:(float)aDefaultTabInterval +{ + _defaultTabInterval = aDefaultTabInterval; +} - (void)addTabStop:(CPTextTab)aTabStop { - // Copy on write logic would go here if we shared structure, - // but here we just mutate the array. [_tabStops addObject:aTabStop]; } +- (void)removeTabStop:(CPTextTab)aTabStop +{ + [_tabStops removeObject:aTabStop]; +} + - (void)setTabStops:(CPArray)newTabStops { if (_tabStops === newTabStops) return; - _tabStops = [newTabStops copy]; + _tabStops = [newTabStops mutableCopy]; } - (id)copyWithZone:(CPZone)aZone @@ -244,81 +332,3 @@ var _sharedDefaultParagraphStyle = nil; } @end - - -// MARK: - CPCoding - -var CPParagraphStyleLineSpacingKey = @"CPParagraphStyleLineSpacingKey", - CPParagraphStyleParagraphSpacingKey = @"CPParagraphStyleParagraphSpacingKey", - CPParagraphStyleAlignmentKey = @"CPParagraphStyleAlignmentKey", - CPParagraphStyleHeadIndentKey = @"CPParagraphStyleHeadIndentKey", - CPParagraphStyleTailIndentKey = @"CPParagraphStyleTailIndentKey", - CPParagraphStyleFirstLineHeadIndentKey = @"CPParagraphStyleFirstLineHeadIndentKey", - CPParagraphStyleMinimumLineHeightKey = @"CPParagraphStyleMinimumLineHeightKey", - CPParagraphStyleMaximumLineHeightKey = @"CPParagraphStyleMaximumLineHeightKey", - CPParagraphStyleLineBreakModeKey = @"CPParagraphStyleLineBreakModeKey", - CPParagraphStyleTabStopsKey = @"CPParagraphStyleTabStopsKey", - CPParagraphStyleBaseWritingDirectionKey = @"CPParagraphStyleBaseWritingDirectionKey", - CPParagraphStyleLineHeightMultipleKey = @"CPParagraphStyleLineHeightMultipleKey", - CPParagraphStyleParagraphSpacingBeforeKey = @"CPParagraphStyleParagraphSpacingBeforeKey", - CPParagraphStyleDefaultTabIntervalKey = @"CPParagraphStyleDefaultTabIntervalKey"; - -@implementation CPParagraphStyle (CPCoding) - -- (id)initWithCoder:(CPCoder)aCoder -{ - if (self = [super init]) - { - _lineSpacing = [aCoder decodeFloatForKey:CPParagraphStyleLineSpacingKey]; - _paragraphSpacing = [aCoder decodeFloatForKey:CPParagraphStyleParagraphSpacingKey]; - _alignment = [aCoder decodeIntForKey:CPParagraphStyleAlignmentKey]; - _headIndent = [aCoder decodeFloatForKey:CPParagraphStyleHeadIndentKey]; - _tailIndent = [aCoder decodeFloatForKey:CPParagraphStyleTailIndentKey]; - _firstLineHeadIndent = [aCoder decodeFloatForKey:CPParagraphStyleFirstLineHeadIndentKey]; - _minimumLineHeight = [aCoder decodeFloatForKey:CPParagraphStyleMinimumLineHeightKey]; - _maximumLineHeight = [aCoder decodeFloatForKey:CPParagraphStyleMaximumLineHeightKey]; - _lineBreakMode = [aCoder decodeIntForKey:CPParagraphStyleLineBreakModeKey]; - _tabStops = [aCoder decodeObjectForKey:CPParagraphStyleTabStopsKey]; - - // Handle potential missing keys for backward compatibility or new properties - if ([aCoder containsValueForKey:CPParagraphStyleBaseWritingDirectionKey]) - _baseWritingDirection = [aCoder decodeIntForKey:CPParagraphStyleBaseWritingDirectionKey]; - else - _baseWritingDirection = CPWritingDirectionNatural; - - if ([aCoder containsValueForKey:CPParagraphStyleLineHeightMultipleKey]) - _lineHeightMultiple = [aCoder decodeFloatForKey:CPParagraphStyleLineHeightMultipleKey]; - - if ([aCoder containsValueForKey:CPParagraphStyleParagraphSpacingBeforeKey]) - _paragraphSpacingBefore = [aCoder decodeFloatForKey:CPParagraphStyleParagraphSpacingBeforeKey]; - - if ([aCoder containsValueForKey:CPParagraphStyleDefaultTabIntervalKey]) - _defaultTabInterval = [aCoder decodeFloatForKey:CPParagraphStyleDefaultTabIntervalKey]; - else - _defaultTabInterval = kDefaultTabInterval; - - if (!_tabStops) _tabStops = []; - } - return self; -} - -- (void)encodeWithCoder:(CPCoder)aCoder -{ - [aCoder encodeFloat:_lineSpacing forKey:CPParagraphStyleLineSpacingKey]; - [aCoder encodeFloat:_paragraphSpacing forKey:CPParagraphStyleParagraphSpacingKey]; - [aCoder encodeInt:_alignment forKey:CPParagraphStyleAlignmentKey]; - [aCoder encodeFloat:_headIndent forKey:CPParagraphStyleHeadIndentKey]; - [aCoder encodeFloat:_tailIndent forKey:CPParagraphStyleTailIndentKey]; - [aCoder encodeFloat:_firstLineHeadIndent forKey:CPParagraphStyleFirstLineHeadIndentKey]; - [aCoder encodeFloat:_minimumLineHeight forKey:CPParagraphStyleMinimumLineHeightKey]; - [aCoder encodeFloat:_maximumLineHeight forKey:CPParagraphStyleMaximumLineHeightKey]; - [aCoder encodeInt:_lineBreakMode forKey:CPParagraphStyleLineBreakModeKey]; - [aCoder encodeObject:_tabStops forKey:CPParagraphStyleTabStopsKey]; - - [aCoder encodeInt:_baseWritingDirection forKey:CPParagraphStyleBaseWritingDirectionKey]; - [aCoder encodeFloat:_lineHeightMultiple forKey:CPParagraphStyleLineHeightMultipleKey]; - [aCoder encodeFloat:_paragraphSpacingBefore forKey:CPParagraphStyleParagraphSpacingBeforeKey]; - [aCoder encodeFloat:_defaultTabInterval forKey:CPParagraphStyleDefaultTabIntervalKey]; -} - -@end diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index 102bba5ef..1683dc83b 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -1,1086 +1,377 @@ /* + * CPRulerView.j + * AppKit + * + * Created by Daniel Boehringer on 11/01/2014 + * Copyright Daniel Boehringer 2014. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ - CPRulerView.j - Created by Daniel Boehringer on 08/01/2016 - - Copyright Daniel Boehringer 2016. - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - +@import @import "CPView.j" -@import "CPScrollView.j" -@import "CPBezierPath.j" +@import "CPTextField.j" @import "CPColor.j" -@import "CPStringDrawing.j" @import "CPFont.j" -@import "CPImage.j" -@class CPRulerMarker +// Orientations matching AppKit standards +// typedef enum CPRulerOrientation +CPHorizontalRuler = 0, +CPVerticalRuler = 1, +CPRulerOrientationHorizontal = 0, +CPRulerOrientationVertical = 1 -@global CPHorizontalRuler -@global CPVerticalRuler +@class CPRulerView; -// Constants for layout -var DEFAULT_RULE_THICKNESS = 16.0, - DEFAULT_MARKER_THICKNESS = 15.0, - HASH_MARK_THICKNESS_FACTOR = 0.6, - HASH_MARK_WIDTH = 1.0, - LABEL_TEXT_PADDING = 2.0; -var _measurementUnits = nil; +// MARK: - CPRulerMarker (Interactive High-Res DOM Handle) -// ----------------------------------------------------------------------------- -// Helper Class: _CPMeasurementUnit -// ----------------------------------------------------------------------------- - -@implementation _CPMeasurementUnit : CPObject +@implementation CPRulerMarker : CPView { - CPString _name @accessors(property = name); - CPString _abbreviation @accessors(property = abbreviation); - float _pointsPerUnit @accessors(property = pointsPerUnit); - CPArray _stepUpCycle @accessors(property = stepUpCycle); - CPArray _stepDownCycle @accessors(property = stepDownCycle); + CPRulerView _rulerView @accessors(readonly, property=rulerView); + float _imageValue @accessors(property=imageValue); + id _representedObject @accessors(property=representedObject); + CPTextField _label; } -+ (void)initialize +- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation imageValue:(float)anImageValue representedObject:(id)anObject { - if (self !== [_CPMeasurementUnit class]) - return; - - _measurementUnits = []; - - [self registerUnit:[_CPMeasurementUnit inchesMeasurementUnit]]; - [self registerUnit:[_CPMeasurementUnit centimetersMeasurementUnit]]; - [self registerUnit:[_CPMeasurementUnit pointsMeasurementUnit]]; - [self registerUnit:[_CPMeasurementUnit picasMeasurementUnit]]; -} - -+ (_CPMeasurementUnit)measurementUnitWithName:(CPString)name abbreviation:(CPString)abbreviation pointsPerUnit:(float)points stepUpCycle:(CPArray)upCycle stepDownCycle:(CPArray)downCycle -{ - return [[self alloc] initWithName:name abbreviation:abbreviation pointsPerUnit:points stepUpCycle:upCycle stepDownCycle:downCycle]; -} - -- (id)initWithName:(CPString)name abbreviation:(CPString)abbreviation pointsPerUnit:(float)points stepUpCycle:(CPArray)upCycle stepDownCycle:(CPArray)downCycle -{ - self = [super init]; - if (self) + // Render a crisp, resizable 12x12 container for the Unicode indicator + if (self = [super initWithFrame:CGRectMake(0, 0, 12, 12)]) { - _name = name; - _abbreviation = abbreviation; - _pointsPerUnit = points; - _stepUpCycle = upCycle; - _stepDownCycle = downCycle; + _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]; } return self; } -+ (_CPMeasurementUnit)inchesMeasurementUnit -{ - return [self measurementUnitWithName:@"Inches" - abbreviation:@"in" - pointsPerUnit:72.0 - stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ] - stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.25], [CPNumber numberWithFloat:0.125] ]]; -} - -+ (_CPMeasurementUnit)centimetersMeasurementUnit -{ - return [self measurementUnitWithName:@"Centimeters" - abbreviation:@"cm" - pointsPerUnit:28.3465 - stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ] - stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.1] ]]; -} - -+ (_CPMeasurementUnit)pointsMeasurementUnit -{ - return [self measurementUnitWithName:@"Points" - abbreviation:@"pt" - pointsPerUnit:1.0 - stepUpCycle:[ [CPNumber numberWithFloat:10.0], [CPNumber numberWithFloat:50.0], [CPNumber numberWithFloat:100.0] ] - stepDownCycle:[ [CPNumber numberWithFloat:0.5] ]]; // Points rarely sub-divide -} - -+ (_CPMeasurementUnit)picasMeasurementUnit -{ - return [self measurementUnitWithName:@"Picas" - abbreviation:@"pc" - pointsPerUnit:12.0 - stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ] - stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.0833] ]]; // 6p and 1pt -} - -+ (CPArray)allMeasurementUnits -{ - return _measurementUnits; -} - -+ (_CPMeasurementUnit)measurementUnitNamed:(CPString)name -{ - var i = 0, count = [_measurementUnits count]; - for (; i < count; ++i) - if ([[[_measurementUnits objectAtIndex:i] name] isEqualToString:name]) - return [_measurementUnits objectAtIndex:i]; - return nil; -} - -+ (void)registerUnit:(_CPMeasurementUnit)unit -{ - if (!_measurementUnits) _measurementUnits = []; - [_measurementUnits addObject:unit]; -} - @end -// ----------------------------------------------------------------------------- -// Class: CPRulerView -// ----------------------------------------------------------------------------- + +// MARK: - CPRulerView (Pure DOM + Interactive Engine) @implementation CPRulerView : CPView { - CPScrollView _scrollView; - CPView _clientView; - CPView _accessoryView; + CPScrollView _scrollView @accessors(property=scrollView); + CPRulerOrientation _orientation @accessors(property=orientation); + CPView _clientView @accessors(property=clientView); + + float _ruleThickness @accessors(property=ruleThickness); + float _reservedThicknessForMarkers; CPArray _markers; - _CPMeasurementUnit _measurementUnit; - CPMutableArray _rulerlineLocations; - float _originOffset; - float _ruleThickness; - float _thicknessForMarkers; - float _thicknessForAccessoryView; - - CPRulerOrientation _orientation; - - // Cache attributes - CPDictionary _labelAttributes; + // Dragger variables + CPRulerMarker _draggingMarker; + CGPoint _dragStartPoint; + float _dragStartLocation; } -// MARK: - Class Methods - -+ (void)registerUnitWithName:(CPString)name abbreviation:(CPString)abbreviation unitToPointsConversionFactor:(float)conversionFactor stepUpCycle:(CPArray)stepUpCycle stepDownCycle:(CPArray)stepDownCycle +- (id)initWithScrollView:(CPScrollView)aScrollView orientation:(CPRulerOrientation)anOrientation { - [_CPMeasurementUnit registerUnit:[_CPMeasurementUnit measurementUnitWithName:name abbreviation:abbreviation pointsPerUnit:conversionFactor stepUpCycle:stepUpCycle stepDownCycle:stepDownCycle]]; -} - -// MARK: - Initialization - -- (id)initWithFrame:(CPRect)frame -{ - return [self initWithScrollView:nil orientation:CPHorizontalRuler]; -} - -- (id)initWithScrollView:(CPScrollView)scrollView orientation:(CPRulerOrientation)orientation -{ - var frame = CPMakeRect(0, 0, 1, 1); - - // Determine initial frame based on standard thickness - if (orientation == CPHorizontalRuler) - frame.size.height = DEFAULT_RULE_THICKNESS; - else - frame.size.width = DEFAULT_RULE_THICKNESS; - - self = [super initWithFrame:frame]; - - if (self) + if (self = [super initWithFrame:CGRectMakeZero()]) { - _scrollView = scrollView; - _orientation = orientation; - _measurementUnit = [_CPMeasurementUnit measurementUnitNamed:@"Inches"]; - - _ruleThickness = DEFAULT_RULE_THICKNESS; - _thicknessForMarkers = 0.0; // Grows as needed - _thicknessForAccessoryView = 0.0; - _originOffset = 0.0; + _scrollView = aScrollView; + _orientation = anOrientation; + _clientView = [aScrollView documentView]; + _ruleThickness = (anOrientation === CPHorizontalRuler) ? 16.0 : 24.0; + _reservedThicknessForMarkers = 0.0; _markers = []; - _rulerlineLocations = []; - - var style = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; - [style setLineBreakMode:CPLineBreakByClipping]; - [style setAlignment:CPLeftTextAlignment]; - _labelAttributes = @{ - CPFontAttributeName: [CPFont systemFontOfSize:9.0], - CPParagraphStyleAttributeName: style, - CPForegroundColorAttributeName: [CPColor blackColor] - }; - } - - return self; -} - -- (id)initWithCoder:(CPCoder)aCoder -{ - self = [super initWithCoder:aCoder]; - if (self) - { - _scrollView = [aCoder decodeObjectForKey:@"CPScrollView"]; - _orientation = [aCoder decodeIntForKey:@"CPOrientation"]; - _markers = [aCoder decodeObjectForKey:@"CPMarkers"]; - if (!_markers) _markers = []; - // Defaults - _ruleThickness = DEFAULT_RULE_THICKNESS; - _measurementUnit = [_CPMeasurementUnit measurementUnitNamed:@"Inches"]; - _rulerlineLocations = []; + [self setBackgroundColor:[CPColor colorWithWhite:0.96 alpha:1.0]]; } return self; } -// MARK: - Layout & Metrics - -- (float)reservedThicknessForMarkers +- (void)setFrame:(CGRect)aFrame { - if ([_markers count] > 0 && _thicknessForMarkers < DEFAULT_MARKER_THICKNESS) - return DEFAULT_MARKER_THICKNESS; + [super setFrame:aFrame]; + [self updateRuler]; +} + +// Markers registration +- (void)addMarker:(CPRulerMarker)aMarker +{ + if ([_markers containsObject:aMarker]) + return; - return _thicknessForMarkers; + [_markers addObject:aMarker]; + [self addSubview:aMarker]; + [self _positionMarker:aMarker]; } -- (float)reservedThicknessForAccessoryView +- (void)removeMarker:(CPRulerMarker)aMarker { - return _thicknessForAccessoryView; + [_markers removeObject:aMarker]; + [aMarker removeFromSuperview]; } -- (float)ruleThickness +- (void)setMarkers:(CPArray)newMarkers { - return _ruleThickness; -} - -- (float)requiredThickness -{ - var result = [self ruleThickness]; - - if ([_markers count] > 0) - result += [self reservedThicknessForMarkers]; - - if (_accessoryView) - result += [self reservedThicknessForAccessoryView]; - - return result; -} - -- (float)baselineLocation -{ - // The baseline is the line separating the ruler from the content. - // In a horizontal ruler, it's the bottom edge (height). - // Usually, the hash marks grow upwards from this baseline. - return [self bounds].size.height; -} - -// MARK: - Accessors - -- (CPScrollView)scrollView -{ - return _scrollView; -} - -- (void)setScrollView:(CPScrollView)scrollView -{ - _scrollView = scrollView; - [self setNeedsDisplay:YES]; -} - -- (CPRulerOrientation)orientation -{ - return _orientation; -} - -- (void)setOrientation:(CPRulerOrientation)orientation -{ - _orientation = orientation; - [self setNeedsDisplay:YES]; -} - -- (CPView)clientView -{ - return _clientView; -} - -- (void)setClientView:(CPView)view -{ - if (_clientView === view) return; + for (var i = 0; i < [_markers count]; i++) + [[_markers objectAtIndex:i] removeFromSuperview]; - if ([_clientView respondsToSelector:@selector(rulerView:willSetClientView:)]) - [_clientView rulerView:self willSetClientView:view]; - - // Standard behavior: clear markers when client changes unless preserved - [_markers removeAllObjects]; - _clientView = view; - - [self invalidateHashMarks]; - [[self enclosingScrollView] tile]; -} - -- (CPView)accessoryView -{ - return _accessoryView; -} - -- (void)setAccessoryView:(CPView)view -{ - if (_accessoryView === view) return; + _markers = [newMarkers mutableCopy]; - [_accessoryView removeFromSuperview]; - _accessoryView = view; - - if (_accessoryView) - { - [self addSubview:_accessoryView]; - // Usually you'd set the frame here based on thickness - } - - [[self enclosingScrollView] tile]; -} - -- (CPArray)markers -{ - return _markers; -} - -- (void)setMarkers:(CPArray)markers -{ - if (_markers === markers) return; - _markers = [markers mutableCopy]; - [[self enclosingScrollView] tile]; - [self setNeedsDisplay:YES]; -} - -- (void)addMarker:(CPRulerMarker)marker -{ - [_markers addObject:marker]; - [marker setRulerView:self]; // Ensure marker knows its ruler - [[self enclosingScrollView] tile]; - [self setNeedsDisplay:YES]; -} - -- (void)removeMarker:(CPRulerMarker)marker -{ - [_markers removeObject:marker]; - [[self enclosingScrollView] tile]; - [self setNeedsDisplay:YES]; -} - -- (void)setMeasurementUnits:(CPString)unitName -{ - var unit = [_CPMeasurementUnit measurementUnitNamed:unitName]; - if (unit) - { - _measurementUnit = unit; - [self setNeedsDisplay:YES]; - } -} - -- (CPString)measurementUnits -{ - return [_measurementUnit name]; -} - -- (void)setOriginOffset:(float)value -{ - _originOffset = value; - [self setNeedsDisplay:YES]; -} - -- (float)originOffset -{ - return _originOffset; -} - -- (void)setRuleThickness:(float)value -{ - _ruleThickness = value; - [[self enclosingScrollView] tile]; -} - -- (void)setReservedThicknessForMarkers:(float)value -{ - _thicknessForMarkers = value; - [[self enclosingScrollView] tile]; -} - -- (void)setReservedThicknessForAccessoryView:(float)value -{ - _thicknessForAccessoryView = value; - [[self enclosingScrollView] tile]; -} - -// MARK: - Event Handling - -- (BOOL)trackMarker:(CPRulerMarker)marker withMouseEvent:(CPEvent)event -{ - // Convert event location to ruler coordinates - var point = [self convertPoint:[event locationInWindow] fromView:nil]; - - // Basic hit testing logic - if (CPPointInRect(point, [self bounds])) - { - // Marker implements trackMouse:adding: - if ([marker respondsToSelector:@selector(trackMouse:adding:)]) - { - [marker trackMouse:event adding:YES]; - } - [self setNeedsDisplay:YES]; - return YES; - } - - return NO; -} - -- (void)mouseDown:(CPEvent)event -{ - var point = [self convertPoint:[event locationInWindow] fromView:nil], - i = 0, count = [_markers count]; - - // 1. Check if an existing marker was clicked - for (; i < count; ++i) + for (var i = 0; i < [_markers count]; i++) { var marker = [_markers objectAtIndex:i]; - if (CPPointInRect(point, [marker imageRectInRuler])) - { - [marker trackMouse:event adding:NO]; - [self setNeedsDisplay:YES]; - return; - } + [self addSubview:marker]; + [self _positionMarker:marker]; } - - // 2. Delegate to Client View (e.g. to create a new guide/marker) - if ([_clientView respondsToSelector:@selector(rulerView:handleMouseDown:)]) - [_clientView rulerView:self handleMouseDown:event]; } -// MARK: - Drawing Support - -- (void)moveRulerlineFromLocation:(float)fromLocation toLocation:(float)toLocation +- (CPRulerMarker)_markerAtPoint:(CGPoint)aPoint { - var oldLoc = [CPNumber numberWithFloat:fromLocation], - newLoc = [CPNumber numberWithFloat:toLocation]; - - [_rulerlineLocations removeObject:oldLoc]; - - // Only add if it's not effectively "off" (using -1 as a convention for hiding) - if (toLocation >= 0) - [_rulerlineLocations addObject:newLoc]; - - [self setNeedsDisplay:YES]; -} - -- (void)invalidateHashMarks -{ - [self setNeedsDisplay:YES]; -} - -// MARK: - Drawing Implementation - -- (BOOL)isFlipped -{ - if (_orientation == CPHorizontalRuler) - return YES; // Horizontal rulers usually draw top-down or match standard flipped views - - // For vertical rulers, we usually want to match the document view's flip state - // so numbers increase going down if the document does. - if (_clientView) - return [_clientView isFlipped]; - - return YES; -} - -- (float)_drawingScale -{ - // Zoom support - var scale = 1.0, - docView = [_scrollView documentView]; - - if (docView && [docView superview]) + for (var i = 0; i < [_markers count]; i++) { - // Calculate scale based on bounds vs frame - var bounds = [docView bounds], - frame = [docView frame]; - - if (_orientation == CPHorizontalRuler) - scale = frame.size.width / bounds.size.width; - else - scale = frame.size.height / bounds.size.height; + var marker = [_markers objectAtIndex:i]; + if (CGRectContainsPoint([marker frame], aPoint)) + return marker; } - return scale; + return nil; } -- (float)_drawingOrigin +- (void)_positionMarker:(CPRulerMarker)aMarker { - // Calculate the point in the ruler that corresponds to "0" in the client view - var origin = 0.0, - trackedView = _clientView; + if (!_scrollView) + return; - if (!trackedView) - trackedView = [_scrollView documentView]; - - if (!trackedView) return 0.0; + var clipView = [_scrollView contentView], + scrollPoint = [clipView bounds].origin, + isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal), + rulerHeight = CGRectGetHeight([self bounds]), + rulerWidth = CGRectGetWidth([self bounds]), + markerLocation = [aMarker imageValue]; - // Convert (0,0) of the tracked view to the ruler's coordinate space - var viewZero = [self convertPoint:CGPointMake(0,0) fromView:trackedView]; - - if (_orientation == CPHorizontalRuler) - origin = viewZero.x; - else - origin = viewZero.y; - - // Apply user-defined offset (e.g. if the user dragged the zero-point) - // Note: originOffset is in client coordinates, so we scale it. - origin += (_originOffset * [self _drawingScale]); - - return origin; -} - -- (void)drawHashMarksAndLabelsInRect:(CPRect)dirtyRect -{ - var bounds = [self bounds], - scale = [self _drawingScale], - zeroLocation = [self _drawingOrigin], // x position (horiz) or y position (vert) where 0 is - pointsPerUnit = [_measurementUnit pointsPerUnit] * scale; - - // Avoid divide by zero - if (pointsPerUnit <= 0) return; - - var isHorizontal = (_orientation == CPHorizontalRuler), - ruleSize = isHorizontal ? bounds.size.height : bounds.size.width; - - // Adjust for markers/accessory thickness area - var reserved = 0.0; - if ([_markers count] > 0) reserved += [self reservedThicknessForMarkers]; - if (_accessoryView) reserved += [self reservedThicknessForAccessoryView]; - - // The actual area for hashes - var hashAreaSize = ruleSize - reserved, - hashBaseline = reserved; // Drawing starts after reserved area - - // Calculate range of units to draw based on dirtyRect - var startPos = isHorizontal ? dirtyRect.origin.x : dirtyRect.origin.y, - endPos = isHorizontal ? CPReectGetMaxX(dirtyRect) : CPReectGetMaxY(dirtyRect); - - // Convert view coordinates to Unit coordinates - // pos = zeroLocation + (unit * pointsPerUnit) - // unit = (pos - zeroLocation) / pointsPerUnit - var startUnit = Math.floor((startPos - zeroLocation) / pointsPerUnit), - endUnit = Math.ceil((endPos - zeroLocation) / pointsPerUnit); - - var stepDowns = [_measurementUnit stepDownCycle], - numSteps = [stepDowns count]; - - [[CPColor grayColor] setStroke]; - - // Iterate through units - for (var u = startUnit; u <= endUnit; u++) - { - var unitPos = zeroLocation + (u * pointsPerUnit); - - // 1. Draw Major Mark - var majorHeight = hashAreaSize * HASH_MARK_THICKNESS_FACTOR; - [self _drawHashAt:unitPos length:majorHeight offset:hashBaseline horizontal:isHorizontal]; - - // 2. Draw Label (only for major units) - var labelStr = [CPString stringWithFormat:@"%d", u]; - - // Simple label positioning - var labelPoint; - if (isHorizontal) - labelPoint = CPMakePoint(unitPos + LABEL_TEXT_PADDING, hashBaseline); - else - labelPoint = CPMakePoint(hashBaseline + LABEL_TEXT_PADDING, unitPos); - - [labelStr drawAtPoint:labelPoint withAttributes:_labelAttributes]; - - // 3. Draw Subdivisions - // We handle a simple single-level subdivision for performance, - // or iterate the cycle. Let's do a simple iterative cycle. - - // E.g. stepDownCycle: [0.5, 0.25, 0.125] implies 1/2, then 1/4, etc. - // But the CPMeasurementUnit spec in the prompt suggests relative steps. - // Let's assume the array contains fractions of the unit: e.g. [0.5, 0.1] - - for (var s = 0; s < numSteps; s++) - { - var stepFraction = [[stepDowns objectAtIndex:s] floatValue]; - if (stepFraction <= 0) continue; - - // Determine visual spacing. If marks are too close, stop recursing. - var pixelsPerStep = pointsPerUnit * stepFraction; - if (pixelsPerStep < 4.0) break; - - // Calculate height: gradually smaller - var subHeight = majorHeight * (0.7 / (s + 1)); - - // How many marks fit in one unit? 1 / fraction. - // e.g. 0.5 -> 2 marks (at 0.0 and 0.5). 0.0 is major, so we draw at 0.5. - var subCount = Math.round(1.0 / stepFraction); - - for (var k = 1; k < subCount; k++) - { - // We only draw if this position wasn't covered by a higher-level step. - // Simplified: Just draw everything, simpler logic for UI. - // Optimization: Skip if integer check passes? No, keep it simple. - - var subUnitOffset = k * stepFraction; - // Only draw if this isn't a whole integer (covered by major) - if (subUnitOffset % 1.0 === 0) continue; - - var subPos = unitPos + (subUnitOffset * pointsPerUnit); - - // Bounds check optimization - if (subPos < startPos || subPos > endPos) continue; - - [self _drawHashAt:subPos length:subHeight offset:hashBaseline horizontal:isHorizontal]; - } - } - } -} - -- (void)_drawHashAt:(float)pos length:(float)length offset:(float)offset horizontal:(BOOL)isHorizontal -{ - var path = [CPBezierPath bezierPath]; - [path setLineWidth:1.0]; - if (isHorizontal) { - // Draw vertical line at 'pos' - [path moveToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height - length)]; // Draw from bottom up - [path lineToPoint:CPMakePoint(pos + 0.5, [self bounds].size.height)]; - - // Or if flipped (top-down), draw from offset down - // Since we force isFlipped=YES for horizontal, y=0 is top. - // Typically rulers align marks to the edge touching the content. - [path moveToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height - length)]; - [path lineToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height)]; - } - else - { - // Draw horizontal line at 'pos' - [path moveToPoint:CPMakePoint(offset + [self bounds].size.width - length, pos + 0.5)]; - [path lineToPoint:CPMakePoint(offset + [self bounds].size.width, pos + 0.5)]; - } - - [path stroke]; -} - -- (void)drawMarkersInRect:(CPRect)dirtyRect -{ - var count = [_markers count]; - for (var i = 0; i < count; i++) - { - var m = [_markers objectAtIndex:i]; - if (CPRectIntersectsRect([m imageRectInRuler], dirtyRect)) - [m drawRect:dirtyRect]; - } -} - -- (void)drawRulerlineLocationsInRect:(CPRect)rect -{ - var count = [_rulerlineLocations count]; - if (count === 0) return; - - [[CPColor controlShadowColor] setStroke]; - - var bounds = [self bounds]; - - for (var i = 0; i < count; ++i) - { - var loc = [[_rulerlineLocations objectAtIndex:i] floatValue]; - - var path = [CPBezierPath bezierPath]; - if (_orientation == CPHorizontalRuler) - { - [path moveToPoint:CPMakePoint(loc + 0.5, 0)]; - [path lineToPoint:CPMakePoint(loc + 0.5, bounds.size.height)]; - } - else - { - [path moveToPoint:CPMakePoint(0, loc + 0.5)]; - [path lineToPoint:CPMakePoint(bounds.size.width, loc + 0.5)]; - } - [path stroke]; - } -} - -- (void)drawRect:(CPRect)dirtyRect -{ - var context = [[CPGraphicsContext currentContext] graphicsPort]; - - // 1. Draw Background - [[CPColor colorWithCalibratedWhite:0.96 alpha:1.0] setFill]; - CGContextFillRect(context, dirtyRect); - - // 2. Draw Bottom/Right Border (Baseline) - [[CPColor darkGrayColor] setStroke]; - var borderPath = [CPBezierPath bezierPath]; - var bounds = [self bounds]; - - if (_orientation == CPHorizontalRuler) - { - [borderPath moveToPoint:CPMakePoint(0, bounds.size.height - 0.5)]; - [borderPath lineToPoint:CPMakePoint(bounds.size.width, bounds.size.height - 0.5)]; - } - else - { - [borderPath moveToPoint:CPMakePoint(bounds.size.width - 0.5, 0)]; - [borderPath lineToPoint:CPMakePoint(bounds.size.width - 0.5, bounds.size.height)]; - } - [borderPath stroke]; - - // 3. Draw Contents - [self drawHashMarksAndLabelsInRect:dirtyRect]; - - if ([_markers count] > 0) - [self drawMarkersInRect:dirtyRect]; - - if ([_rulerlineLocations count] > 0) - [self drawRulerlineLocationsInRect:dirtyRect]; -} - -@end - -@implementation CPRulerMarker : CPObject -{ - CPRulerView _ruler; - float _markerLocation; - CPImage _image; - CPPoint _imageOrigin; - - BOOL _movable @accessors(property=movable); - BOOL _removable @accessors(property=removable); - - id _representedObject @accessors(property=representedObject); - - // Internal state - BOOL _dragging @accessors(getter=isDragging); -} - -// MARK: - Initialization - -- (id)initWithRulerView:(CPRulerView)aRuler markerLocation:(float)location image:(CPImage)anImage imageOrigin:(CPPoint)anImageOrigin -{ - self = [super init]; - if (self) - { - _ruler = aRuler; - _markerLocation = location; - _image = anImage; - _imageOrigin = anImageOrigin; - - _movable = NO; - _removable = NO; - _dragging = NO; - } - return self; -} - -- (id)initWithCoder:(CPCoder)aCoder -{ - self = [super init]; - if (self) - { - // Ruler is usually assigned after decoding by the view hierarchy - _markerLocation = [aCoder decodeFloatForKey:@"CPMarkerLocation"]; - _image = [aCoder decodeObjectForKey:@"CPImage"]; - _imageOrigin = [aCoder decodePointForKey:@"CPImageOrigin"]; - _movable = [aCoder decodeBoolForKey:@"CPMovable"]; - _removable = [aCoder decodeBoolForKey:@"CPRemovable"]; - _representedObject = [aCoder decodeObjectForKey:@"CPRepresentedObject"]; - } - return self; -} - -- (void)encodeWithCoder:(CPCoder)aCoder -{ - [aCoder encodeFloat:_markerLocation forKey:@"CPMarkerLocation"]; - [aCoder encodeObject:_image forKey:@"CPImage"]; - [aCoder encodePoint:_imageOrigin forKey:@"CPImageOrigin"]; - [aCoder encodeBool:_movable forKey:@"CPMovable"]; - [aCoder encodeBool:_removable forKey:@"CPRemovable"]; - [aCoder encodeObject:_representedObject forKey:@"CPRepresentedObject"]; -} - -- (id)copy -{ - var copy = [[CPRulerMarker alloc] initWithRulerView:_ruler - markerLocation:_markerLocation - image:_image - imageOrigin:_imageOrigin]; - [copy setMovable:_movable]; - [copy setRemovable:_removable]; - [copy setRepresentedObject:_representedObject]; - return copy; -} - -// MARK: - Accessors - -- (CPRulerView)ruler -{ - return _ruler; -} - -// Used when adding a marker to a ruler -- (void)setRulerView:(CPRulerView)aRuler -{ - _ruler = aRuler; -} - -- (CPImage)image -{ - return _image; -} - -- (void)setImage:(CPImage)anImage -{ - _image = anImage; - [_ruler setNeedsDisplay:YES]; -} - -- (CPPoint)imageOrigin -{ - return _imageOrigin; -} - -- (void)setImageOrigin:(CPPoint)aPoint -{ - _imageOrigin = aPoint; - [_ruler setNeedsDisplay:YES]; -} - -- (float)markerLocation -{ - return _markerLocation; -} - -- (void)setMarkerLocation:(float)location -{ - if (_markerLocation === location) return; - - _markerLocation = location; - - // Invalidate the ruler to redraw at new position - [_ruler setNeedsDisplay:YES]; -} - -// MARK: - Geometry - -- (float)thicknessRequiredInRuler -{ - if (!_image) return 0.0; - - // If horizontal ruler, thickness is image height. - // If vertical ruler, thickness is image width. - var size = [_image size]; - - if ([_ruler orientation] === CPHorizontalRuler) - return size.height; - else - return size.width; -} - -- (CPRect)imageRectInRuler -{ - if (!_ruler || !_image) return CPMakeRect(0,0,0,0); - - var clientView = [_ruler clientView]; - // If no client view, fallback to document view or 0 - if (!clientView) clientView = [[_ruler scrollView] documentView]; - - var rulerBounds = [_ruler bounds], - imageSize = [_image size], - rect = CPMakeRect(0, 0, imageSize.width, imageSize.height); - - // 1. Convert Marker Location (Client Coords) to Ruler Coords - var locationInRuler = 0.0; - - if (clientView) - { - // We create a point in the client view at the marker location - var ptInClient = CPMakePoint(0, 0); - - if ([_ruler orientation] === CPHorizontalRuler) - ptInClient.x = _markerLocation; - else - ptInClient.y = _markerLocation; + var x = markerLocation - scrollPoint.x - 6.0, // Center the 12px wide marker + y = rulerHeight - 11.0; // Sit perfectly above bottom border - // Convert that point to the ruler's coordinate system - var ptInRuler = [_ruler convertPoint:ptInClient fromView:clientView]; + [aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)]; + } + else + { + var x = rulerWidth - 11.0, + y = markerLocation - scrollPoint.y - 6.0; + + [aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)]; + } +} + + +#pragma mark - +#pragma mark Interaction Handlers + +- (void)mouseDown:(CPEvent)anEvent +{ + var locationInWindow = [anEvent locationInWindow], + localPoint = [self convertPoint:locationInWindow fromView:nil], + clipView = [_scrollView contentView], + scrollPoint = [clipView bounds].origin, + isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal); + + var rulerLocation = isHorizontal ? (localPoint.x + scrollPoint.x) : (localPoint.y + scrollPoint.y); + + // 1. Check if clicked an existing marker + var clickedMarker = [self _markerAtPoint:localPoint]; + if (clickedMarker) + { + _draggingMarker = clickedMarker; + _dragStartPoint = localPoint; + _dragStartLocation = [_draggingMarker imageValue]; + } + // 2. Otherwise, create a new marker dynamically where the user clicked + else + { + var newMarker = [[CPRulerMarker alloc] initWithRulerView:self + markerLocation:rulerLocation + imageValue:rulerLocation + representedObject:nil]; + [self addMarker:newMarker]; - locationInRuler = ([_ruler orientation] === CPHorizontalRuler) ? ptInRuler.x : ptInRuler.y; + _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]; + } +} + +- (void)mouseDragged:(CPEvent)anEvent +{ + if (!_draggingMarker) + return; + + var locationInWindow = [anEvent locationInWindow], + localPoint = [self convertPoint:locationInWindow fromView:nil], + isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal); + + var delta = isHorizontal ? (localPoint.x - _dragStartPoint.x) : (localPoint.y - _dragStartPoint.y), + newLocation = _dragStartLocation + delta; + + if (newLocation < 0) newLocation = 0; + + [_draggingMarker setImageValue:newLocation]; + [self _positionMarker:_draggingMarker]; + + // Notify the CPTextView that the marker coordinates shifted + var client = [self clientView]; + if (client && [client respondsToSelector:@selector(rulerView:didMoveMarker:)]) + [client rulerView:self didMoveMarker:_draggingMarker]; +} + +- (void)mouseUp:(CPEvent)anEvent +{ + if (!_draggingMarker) + return; + + var localPoint = [self convertPoint:[anEvent locationInWindow] fromView:nil], + isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal), + + // If dragged more than 15 pixels off the ruler, delete the marker + draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15) + : (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15); + + if (draggedOff) + { + var client = [self clientView]; + if (client && [client respondsToSelector:@selector(rulerView:didRemoveMarker:)]) + [client rulerView:self didRemoveMarker:_draggingMarker]; + + [self removeMarker:_draggingMarker]; } - // Apply user offset from CPRulerView (originOffset) - // Note: The conversion above usually handles view transforms, but if CPRulerView - // manually applies an extra offset property (like in the previous implementation), - // we should respect it implicitly via the conversion or explicitly here. - // Assuming standard view conversion handles the scroll/bounds. - - // 2. Position the rect based on Orientation and Image Origin - // imageOrigin is the point *inside* the image that aligns with 'locationInRuler' - - if ([_ruler orientation] === CPHorizontalRuler) + _draggingMarker = nil; +} + + +#pragma mark - +#pragma mark DOM Layout Builder + +- (void)updateRuler +{ + // Wipe subviews to redraw the dynamic visible tick lines/numbers + [self setSubviews:@[]]; + + if (!_scrollView) + return; + + var clipView = [_scrollView contentView], + scrollBounds = [clipView bounds], + scrollPoint = scrollBounds.origin, + visibleSize = scrollBounds.size, + isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal); + + if (isHorizontal) { - // X: Aligned by location minus the hotspot X - rect.origin.x = locationInRuler - _imageOrigin.x; - - // Y: This depends on the ruler's baseline. - // Typically horizontal markers sit on the bottom edge (baseline). - // If the image origin Y is the "tip", we position relative to that. - // Usually, imageOrigin.y is 0 (bottom) or Height (top) depending on coordinate flip. - - // Assuming the ruler draws labels near the baseline and the marker sits on it: - // Let's assume the baseline is the bottom of the ruler rect. - var baseline = [_ruler baselineLocation]; - rect.origin.y = baseline - _imageOrigin.y; - - // If flipped, adjustments might be needed, but usually image drawing handles internal flip. + var start = Math.floor(scrollPoint.x / 10) * 10, + end = scrollPoint.x + visibleSize.width, + rulerHeight = CGRectGetHeight([self bounds]); + + // Draw solid horizontal bottom border (pure, razor-sharp CSS DOM view) + var bottomBorder = [[CPView alloc] initWithFrame:CGRectMake(0, rulerHeight - 1, visibleSize.width, 1)]; + [bottomBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; + [self addSubview:bottomBorder]; + + for (var val = start; val <= end; val += 10) + { + if (val < 0) continue; + + var screenX = val - scrollPoint.x, + isMajor = (val % 50 === 0), + tickHeight = isMajor ? 8.0 : 4.0, + tickY = rulerHeight - tickHeight - 1.0; + + // Tick mark CSS line view + var tick = [[CPView alloc] initWithFrame:CGRectMake(screenX, tickY, 1.0, tickHeight)]; + [tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]]; + [self addSubview:tick]; + + // Unit label + if (isMajor) + { + var label = [[CPTextField alloc] initWithFrame:CGRectMake(screenX - 20.0, 1.0, 40.0, 12.0)]; + [label setStringValue:[CPString stringWithFormat:@"%d", val]]; + [label setFont:[CPFont systemFontOfSize:8.0]]; + [label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]]; + [label setAlignment:CPCenterTextAlignment]; + [self addSubview:label]; + } + } } else { // Vertical Ruler - // Y: Aligned by location minus hotspot Y - rect.origin.y = locationInRuler - _imageOrigin.y; - - // X: Align to right edge (baseline) - var baseline = [_ruler baselineLocation]; // Width for vertical ruler - rect.origin.x = baseline - _imageOrigin.x; - } + var start = Math.floor(scrollPoint.y / 10) * 10, + end = scrollPoint.y + visibleSize.height, + rulerWidth = CGRectGetWidth([self bounds]); - return rect; -} + // Draw solid vertical right border (pure DOM) + var rightBorder = [[CPView alloc] initWithFrame:CGRectMake(rulerWidth - 1, 0, 1, visibleSize.height)]; + [rightBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; + [self addSubview:rightBorder]; -// MARK: - Drawing - -- (void)drawRect:(CPRect)aRect -{ - if (!_image) return; - - var rect = [self imageRectInRuler]; - - // Only draw if visible - if (CPRectIntersectsRect(rect, aRect)) - { - // Visual feedback for dragging - var opacity = _dragging ? 0.5 : 1.0; - - [_image drawInRect:rect fromRect:CPZeroRect operation:CPCompositeSourceOver fraction:opacity]; - } -} - -// MARK: - Event Handling - -- (BOOL)trackMouse:(CPEvent)anEvent adding:(BOOL)adding -{ - if (!adding && !_movable) return NO; - - var clientView = [_ruler clientView], - delegate = clientView; // The client view acts as the delegate usually - - // Delegate Check: Should we move/add? - if (adding) - { - if ([delegate respondsToSelector:@selector(rulerView:shouldAddMarker:)] && - ![delegate rulerView:_ruler shouldAddMarker:self]) - return NO; - } - else - { - if ([delegate respondsToSelector:@selector(rulerView:shouldMoveMarker:)] && - ![delegate rulerView:_ruler shouldMoveMarker:self]) - return NO; - } - - // If adding, we ensure the marker is in the array so it draws - if (adding) - [_ruler addMarker:self]; - - [self setDragging:YES]; - - var type = [anEvent type], - originalLocation = _markerLocation; - - // Start Event Loop - while (type !== CPLeftMouseUp) - { - // 1. Calculate new location - var locationInWindow = [anEvent locationInWindow], - pointInClient = [clientView convertPoint:locationInWindow fromView:nil], - newLocation = ([_ruler orientation] === CPHorizontalRuler) ? pointInClient.x : pointInClient.y; - - // 2. Delegate: Will Move? (Snap logic usually happens here) - if ([delegate respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)]) + for (var val = start; val <= end; val += 10) { - newLocation = [delegate rulerView:_ruler willMoveMarker:self toLocation:newLocation]; - } - - // 3. Update Location - [self setMarkerLocation:newLocation]; - - // 4. Handle "Tearing off" (Removal visual feedback) - // Check if mouse is far outside the ruler's bounds - var pointInRuler = [_ruler convertPoint:locationInWindow fromView:nil]; - var rulerBounds = [_ruler bounds]; - // Expand bounds slightly for tolerance - var expandedBounds = CPMakeRect(rulerBounds.origin.x - 20, rulerBounds.origin.y - 20, - rulerBounds.size.width + 40, rulerBounds.size.height + 40); - - var isFarAway = !CPPointInRect(pointInRuler, expandedBounds); - - // You might change the cursor here to a "poof" cursor if isFarAway && _removable - - // 5. Get next event - anEvent = [[CPApp currentEvent] window] ? [[CPApp currentEvent] window] : [CPApp keyWindow]; // fallback - anEvent = [CPApp nextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask - untilDate:[CPDate distantFuture] - inMode:CPDefaultRunLoopMode - dequeue:YES]; - type = [anEvent type]; - } - - [self setDragging:NO]; - - // Finalize - var pointInRuler = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil], - rulerBounds = [_ruler bounds], - expandedBounds = CPMakeRect(rulerBounds.origin.x - 10, rulerBounds.origin.y - 10, - rulerBounds.size.width + 20, rulerBounds.size.height + 20), - shouldRemove = _removable && !CPPointInRect(pointInRuler, expandedBounds); + if (val < 0) continue; - if (shouldRemove) - { - var allowed = YES; - if ([delegate respondsToSelector:@selector(rulerView:shouldRemoveMarker:)]) - allowed = [delegate rulerView:_ruler shouldRemoveMarker:self]; - - if (allowed) - { - [_ruler removeMarker:self]; - // Don't call didMove or didAdd if removed - return YES; + var screenY = val - scrollPoint.y, + isMajor = (val % 50 === 0), + tickWidth = isMajor ? 8.0 : 4.0, + tickX = rulerWidth - tickWidth - 1.0; + + // Tick mark CSS line view + var tick = [[CPView alloc] initWithFrame:CGRectMake(tickX, screenY, tickWidth, 1.0)]; + [tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]]; + [self addSubview:tick]; + + // Unit label + if (isMajor) + { + var label = [[CPTextField alloc] initWithFrame:CGRectMake(1.0, screenY - 6.0, rulerWidth - 12.0, 12.0)]; + [label setStringValue:[CPString stringWithFormat:@"%d", val]]; + [label setFont:[CPFont systemFontOfSize:8.0]]; + [label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]]; + [label setAlignment:CPRightTextAlignment]; + [self addSubview:label]; + } } } - // Success notification - if (adding) + // Reposition and display active markers + for (var i = 0; i < [_markers count]; i++) { - if ([delegate respondsToSelector:@selector(rulerView:didAddMarker:)]) - [delegate rulerView:_ruler didAddMarker:self]; + var marker = [_markers objectAtIndex:i]; + if ([marker superview] !== self) + [self addSubview:marker]; + [self _positionMarker:marker]; } - else - { - if ([delegate respondsToSelector:@selector(rulerView:didMoveMarker:)]) - [delegate rulerView:_ruler didMoveMarker:self]; - } - - return YES; } @end diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 39cf88378..c3b14a058 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -542,12 +542,15 @@ var kDelegateRespondsTo_textShouldBeginEditing - (void)_becomeFirstResponder { [self updateInsertionPointStateAndRestartTimer:YES]; + + // SYNCHRONIZE ACTIVE PARAGRAPH MARKERS ON EDITOR FOCUS + [self updateRuler]; + [[CPFontManager sharedFontManager] setSelectedFont:[self font] isMultiple:NO]; [self setNeedsDisplay:YES]; [[CPRunLoop currentRunLoop] performSelector:@selector(focusForTextView:) target:[_CPNativeInputManager class] argument:self order:0 modes:[CPDefaultRunLoopMode]]; } - - (BOOL)becomeFirstResponder { [super becomeFirstResponder]; @@ -1783,6 +1786,9 @@ Sets the selection to a range of characters in response to user action. [self _enrichEssentialTypingAttributes:_typingAttributes]; } + // SYNCHRONIZE ACTIVE PARAGRAPH MARKERS ON TYPING ATTRIBUTES CHANGE + [self updateRuler]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; // We always clear the saved selection range from the last mouse down event here. @@ -2406,6 +2412,233 @@ Sets the selection to a range of characters in response to user action. @end +@implementation CPTextView (CPRulerSupport) + +- (void)updateRuler +{ + var scrollView = [self enclosingScrollView]; + if (!scrollView || ![scrollView hasHorizontalRuler] || ![scrollView rulersVisible]) + return; + + var ruler = [scrollView horizontalRulerView]; + if (!ruler) + return; + + var selectedRange = [self selectedRange], + paragraphStyle = [CPParagraphStyle defaultParagraphStyle], + currentAttributes = _typingAttributes; + + var textLength = [_textStorage length]; + if (textLength > 0) + { + var charIndex = selectedRange.location; + + // Safety bounds checks for cursor placements + if (charIndex >= textLength) + charIndex = textLength - 1; + if (charIndex < 0) + charIndex = 0; + + currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; + } + + if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) + paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; + + var markers = [], + tabStops = [paragraphStyle tabStops], + count = [tabStops count]; + + // A. Load existing tab stop markers onto the ruler + for (var i = 0; i < count; i++) + { + var tab = [tabStops objectAtIndex:i], + marker = [[CPRulerMarker alloc] initWithRulerView:ruler + markerLocation:[tab location] + imageValue:[tab location] + representedObject:tab]; + [markers addObject:marker]; + } + + // B. Add Indentation Handles (First line indent & Head indent) + var firstLineMarker = [[CPRulerMarker alloc] initWithRulerView:ruler + 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]; +} + +// Local comparison function helper for tab sorting +var compareTabStops = function(obj1, obj2, context) { + if ([obj1 location] < [obj2 location]) return CPOrderedAscending; + if ([obj1 location] > [obj2 location]) return CPOrderedDescending; + return CPOrderedSame; +}; + +- (void)rulerView:(CPRulerView)rulerView didAddMarker:(CPRulerMarker)marker +{ + if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil]) + return; + + var selectedRange = [self selectedRange], + paragraphStyle = [CPParagraphStyle defaultParagraphStyle], + currentAttributes = _typingAttributes; + + if (selectedRange.length > 0) + currentAttributes = [_textStorage attributesAtIndex:selectedRange.location effectiveRange:nil]; + + if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) + paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; + + var mutableStyle = [paragraphStyle mutableCopy]; + + // Create a new Left-aligned tab stop where the user clicked + var newTab = [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:[marker imageValue]], + tabs = [[mutableStyle tabStops] mutableCopy]; + + [tabs addObject:newTab]; + + // Sort tabs by location ascending + [tabs sortUsingFunction:compareTabStops context:nil]; + + [mutableStyle setTabStops:tabs]; + [marker setRepresentedObject:newTab]; + + if (selectedRange.length > 0) + { + [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(selectedRange)]; + + [_layoutManager textStorage:_textStorage + edited:0 + range:CPMakeRangeCopy(selectedRange) + changeInLength:0 + invalidatedRange:CPMakeRangeCopy(selectedRange)]; + } + else + { + [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + } +} + +- (void)rulerView:(CPRulerView)rulerView didMoveMarker:(CPRulerMarker)marker +{ + if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil]) + return; + + var selectedRange = [self selectedRange], + paragraphStyle = [CPParagraphStyle defaultParagraphStyle], + currentAttributes = _typingAttributes; + + if (selectedRange.length > 0) + currentAttributes = [_textStorage attributesAtIndex:selectedRange.location effectiveRange:nil]; + + if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) + paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; + + var mutableStyle = [paragraphStyle mutableCopy], + oldTab = [marker representedObject]; + + if (!oldTab) + return; + + // A. Handle standard tab stops + if ([oldTab isKindOfClass:[CPTextTab class]]) + { + var newTab = [[CPTextTab alloc] initWithType:[oldTab alignment] location:[marker imageValue]], + tabs = [[mutableStyle tabStops] mutableCopy]; + + [tabs removeObject:oldTab]; + [tabs addObject:newTab]; + + // Sort tabs by location ascending + [tabs sortUsingFunction:compareTabStops context:nil]; + + [mutableStyle setTabStops:tabs]; + [marker setRepresentedObject:newTab]; + } + // B. Handle Indentation Marker drags (First Line, Left, and Right indents) + else if ([oldTab isKindOfClass:[CPString class]]) + { + if (oldTab === @"CPFirstLineIndent") + [mutableStyle setFirstLineHeadIndent:[marker imageValue]]; + else if (oldTab === @"CPHeadIndent") + [mutableStyle setHeadIndent:[marker imageValue]]; + else if (oldTab === @"CPTailIndent") + [mutableStyle setTailIndent:[marker imageValue]]; + } + + if (selectedRange.length > 0) + { + [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(selectedRange)]; + + [_layoutManager textStorage:_textStorage + edited:0 + range:CPMakeRangeCopy(selectedRange) + changeInLength:0 + invalidatedRange:CPMakeRangeCopy(selectedRange)]; + } + else + { + [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + } +} + +- (void)rulerView:(CPRulerView)rulerView didRemoveMarker:(CPRulerMarker)marker +{ + if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil]) + return; + + var selectedRange = [self selectedRange], + paragraphStyle = [CPParagraphStyle defaultParagraphStyle], + currentAttributes = _typingAttributes; + + if (selectedRange.length > 0) + currentAttributes = [_textStorage attributesAtIndex:selectedRange.location effectiveRange:nil]; + + if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) + paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; + + var mutableStyle = [paragraphStyle mutableCopy], + oldTab = [marker representedObject]; + + if (!oldTab) + return; + + var tabs = [[mutableStyle tabStops] mutableCopy]; + [tabs removeObject:oldTab]; + + [mutableStyle setTabStops:tabs]; + + if (selectedRange.length > 0) + { + [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(selectedRange)]; + + [_layoutManager textStorage:_textStorage + edited:0 + range:CPMakeRangeCopy(selectedRange) + changeInLength:0 + invalidatedRange:CPMakeRangeCopy(selectedRange)]; + } + else + { + [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + } +} + +@end @implementation CPTextView (CPTextViewDelegate) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index ef793cb86..186ea0a63 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -135,29 +135,34 @@ var CPSystemTypesetterFactory, return [_layoutManager textContainers]; } +// Retrieves correct CPTextTab stop accounting for CPArray properties - (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction { var tabStops = [_currentParagraph tabStops]; if (!tabStops) - tabStops = [CPParagraphStyle _defaultTabStops]; + tabStops = [[CPParagraphStyle defaultParagraphStyle] tabStops]; - var l = tabStops.length; + var l = [tabStops count]; + if (l === 0) + return nil; - if (aWidth > tabStops[l - 1]._location) + var lastTab = [tabStops lastObject]; + if (aWidth > [lastTab location]) return nil; for (var i = l - 1; i >= 0; i--) { - if (aWidth > tabStops[i]._location) + var tab = [tabStops objectAtIndex:i]; + if (aWidth > [tab location]) { if (i + 1 < l) - return tabStops[i + 1]; + return [tabStops objectAtIndex:i + 1]; } } if (i === -1) - return tabStops[0]; + return [tabStops objectAtIndex:0]; return nil; } @@ -356,9 +361,50 @@ var CPSystemTypesetterFactory, isTabStop = YES; if (nextTab) - rangeWidth = nextTab._location - lineOrigin.x; + { + // Look-ahead to measure the width of the incoming text segment for alignment + var nextSegmentWidth = 0.0, + tempIndex = glyphIndex + 1, + segmentString = ""; + + while (tempIndex < numberOfGlyphs) + { + var nextCharCode = theString.charCodeAt(tempIndex); + if (nextCharCode === 9 || nextCharCode === 10 || nextCharCode === 13) + break; + segmentString += theString.charAt(tempIndex); + tempIndex++; + } + + if (segmentString.length > 0) + nextSegmentWidth = [segmentString sizeWithFont:currentFont inWidth:NULL].width; + + var tabLocation = [nextTab location], + tabAlignment = [nextTab alignment]; + + // Mathematically offset the tab character's right boundary + if (tabAlignment === CPCenterTextAlignment) + { + rangeWidth = (tabLocation - nextSegmentWidth / 2.0) - lineOrigin.x; + } + else if (tabAlignment === CPRightTextAlignment) + { + rangeWidth = (tabLocation - nextSegmentWidth) - lineOrigin.x; + } + else // Left align tab stop + { + rangeWidth = tabLocation - lineOrigin.x; + } + + // Enforce a minimum safety spacer width to avoid character overlapping + var minRangeWidth = prevRangeWidth + 5.0; + if (rangeWidth < minRangeWidth) + rangeWidth = minRangeWidth; + } else - rangeWidth += 28; //FIXME + { + rangeWidth += 28.0; // standard fallback spacer + } } // fallthrough intentional case 32: // ' ' wrapRange = CPMakeRangeCopy(lineRange); diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 0d629ce75..43165c3d7 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -75,6 +75,21 @@ [_textView alignJustified:self]; } +- (void)insertAttachment:(id)sender +{ + // Insert modern spinner image attachment + var tempImageView = [[CPImageView alloc] initWithFrame:CGRectMake(0, 0, 32, 32)]; + [tempImageView setImage:[[CPImage alloc] initWithContentsOfFile:@"Resources/spinner.gif" size:CGSizeMake(32, 32)]]; + [_textView insertText:[CPTextStorage attributedStringWithAttachment:tempImageView]]; + [_textView insertText:@" "]; + + // Insert an interactive button attachment + var tempButton = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 80, 28)]; + [tempButton setTitle:@"Click Me"]; + [_textView insertText:[CPTextStorage attributedStringWithAttachment:tempButton]]; + [_textView insertText:@" "]; +} + - (void)applicationDidFinishLaunching:(CPNotification)aNotification { var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask], @@ -112,7 +127,7 @@ [rulerButton setTarget:self]; [rulerButton setAction:@selector(toggleRuler:)]; [toolbarView addSubview:rulerButton]; - currentX += 140; + currentX += 130; // RTF Roundtrip Trigger var rtfButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 150, 30)]; @@ -120,7 +135,15 @@ [rtfButton setTarget:self]; [rtfButton setAction:@selector(makeRTF:)]; [toolbarView addSubview:rtfButton]; - currentX += 165; + currentX += 160; + + // Insert Attachment Trigger + var attachButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 140, 30)]; + [attachButton setTitle:@"Insert Attachment"]; + [attachButton setTarget:self]; + [attachButton setAction:@selector(insertAttachment:)]; + [toolbarView addSubview:attachButton]; + currentX += 150; // Text Alignment Group var labelAlign = [[CPTextField alloc] initWithFrame:CGRectMake(currentX, 22, 45, 20)]; @@ -129,34 +152,34 @@ [toolbarView addSubview:labelAlign]; currentX += 45; - var alignLeftBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 30, 30)]; - [alignLeftBtn setTitle:@"⃔"]; // Left-align symbol + var alignLeftBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 50, 30)]; + [alignLeftBtn setTitle:@"Left"]; [alignLeftBtn setTarget:self]; [alignLeftBtn setAction:@selector(alignLeft:)]; [toolbarView addSubview:alignLeftBtn]; - currentX += 32; + currentX += 55; - var alignCenterBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 30, 30)]; - [alignCenterBtn setTitle:@"↔"]; // Center-align symbol + var alignCenterBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 60, 30)]; + [alignCenterBtn setTitle:@"Center"]; [alignCenterBtn setTarget:self]; [alignCenterBtn setAction:@selector(alignCenter:)]; [toolbarView addSubview:alignCenterBtn]; - currentX += 32; + currentX += 65; - var alignRightBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 30, 30)]; - [alignRightBtn setTitle:@"⃕"]; // Right-align symbol + var alignRightBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 55, 30)]; + [alignRightBtn setTitle:@"Right"]; [alignRightBtn setTarget:self]; [alignRightBtn setAction:@selector(alignRight:)]; [toolbarView addSubview:alignRightBtn]; - currentX += 32; + currentX += 60; - var alignJustifyBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 30, 30)]; - [alignJustifyBtn setTitle:@"≡"]; // Justify-align symbol + var alignJustifyBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 65, 30)]; + [alignJustifyBtn setTitle:@"Justify"]; [alignJustifyBtn setTarget:self]; [alignJustifyBtn setAction:@selector(alignJustified:)]; [toolbarView addSubview:alignJustifyBtn]; - // Default return key target test (as defined in original source) + // Default return key target test var returnButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth([contentView bounds]) - 270, 15, 250, 30)]; [returnButton setAutoresizingMask:CPViewMinXMargin]; [returnButton setTitle:@"Key Return Target"]; @@ -244,44 +267,41 @@ [mainMenu setSubmenu:formatMenu forItem:item]; // 4. Load Rich Sample Text content - [_textView insertText:@"123"]; - var tempImageView = [[CPImageView alloc] initWithFrame:CGRectMake(0, 0, 32, 32)]; - [tempImageView setImage:[[CPImage alloc] initWithContentsOfFile:@"Resources/spinner.gif" size:CGSizeMake(32, 32)]]; + [_textView insertText:@"123 456 "]; - [_textView insertText:[CPTextStorage attributedStringWithAttachment:tempImageView]]; - [_textView insertText:@" 456 "]; - - var tempButton = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 64, 28)]; - [_textView insertText:[CPTextStorage attributedStringWithAttachment:tempButton]]; - - // Centered paragraph text block - var centeredParagraph = [CPParagraphStyle new]; + // Elegant Slate-Blue & Soft Light Blue paragraph + var centeredParagraph = [CPMutableParagraphStyle new]; [centeredParagraph setAlignment:CPCenterTextAlignment]; [_textView insertText:@"\n"]; + + var elegantForeground = [CPColor colorWithRed:0.18 green:0.24 blue:0.35 alpha:1.0]; // Slate Blue + var elegantBackground = [CPColor colorWithRed:0.92 green:0.95 blue:0.98 alpha:1.0]; // Soft Sky Blue tint + [_textView insertText:[[CPAttributedString alloc] initWithString:@"Fusce\n" - attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:@"Arial" size:18], [CPColor redColor], [CPColor yellowColor]] + attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:@"Arial" size:18], elegantForeground, elegantBackground] forKeys:[CPParagraphStyleAttributeName, CPFontAttributeName, CPForegroundColorAttributeName, CPBackgroundColorAttributeName]]]]; - // Highlighted Heading + // Highlighted Heading - Pine & Sage Green tones [_textView insertText:@"\n"]; + var showcaseForeground = [CPColor colorWithRed:0.15 green:0.25 blue:0.15 alpha:1.0]; // Forest Green + var showcaseBackground = [CPColor colorWithRed:0.94 green:0.97 blue:0.92 alpha:1.0]; // Light Sage Green + [_textView insertText:[[CPAttributedString alloc] initWithString:@"Interactive Ruler Showcase\n" - attributes:[CPDictionary dictionaryWithObjects:[[CPFont boldFontWithName:@"Arial" size:22], [CPColor yellowColor]] - forKeys:[CPFontAttributeName, CPBackgroundColorAttributeName]]]]; + attributes:[CPDictionary dictionaryWithObjects:[[CPFont boldFontWithName:@"Arial" size:22], showcaseForeground, showcaseBackground] + forKeys:[CPFontAttributeName, CPForegroundColorAttributeName, CPBackgroundColorAttributeName]]]]; // DEMONSTRATION OF CPRULERVIEW TAB MARKERS - // Creating left, center, and right tabs to showcase the ruler markers dynamically var tabParagraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; - var tab1 = [[CPTextTab alloc] initWithType:CPLeftTabStopType location:100.0]; - var tab2 = [[CPTextTab alloc] initWithType:CPCenterTabStopType location:220.0]; - var tab3 = [[CPTextTab alloc] initWithType:CPRightTabStopType location:340.0]; + var tab1 = [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:100.0]; + var tab2 = [[CPTextTab alloc] initWithType:CPCenterTextAlignment location:220.0]; + var tab3 = [[CPTextTab alloc] initWithType:CPRightTextAlignment location:340.0]; [tabParagraph setTabStops:[tab1, tab2, tab3]]; [_textView insertText:@"\n"]; - [_textView insertText:[[CPAttributedString alloc] initWithString:@"Tab1\tTab2\tTab3\nLeftAlign\tCenterAlign\tRightAlign\n" - attributes:[CPDictionary dictionaryWithObject:tabParagraph forKey:CPParagraphStyleAttributeName]]]; + [_textView insertText:[[CPAttributedString alloc] initWithString:@"\tTab1\tTab2\tTab3\n\tLeftAlign\tCenterAlign\tRightAlign\n" + attributes:[CPDictionary dictionaryWithObject:tabParagraph forKey:CPParagraphStyleAttributeName]]]; // DEMONSTRATION OF INDENTATION MARKERS - // Creating custom margin and indentation settings var indentParagraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; [indentParagraph setFirstLineHeadIndent:30.0]; [indentParagraph setHeadIndent:50.0]; From 41c422ed03ed6195d7698980cb9bd4181619d0b6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 29 May 2026 10:55:22 +0200 Subject: [PATCH 27/39] new: visual feedback when "tearing off" --- AppKit/CPTextView/CPRulerView.j | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index 1683dc83b..dc69a131c 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -240,6 +240,23 @@ CPRulerOrientationVertical = 1 [_draggingMarker setImageValue:newLocation]; [self _positionMarker:_draggingMarker]; + // Check if dragged off the ruler (more than 15px off the boundary) + var draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15) + : (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15); + + if (draggedOff) + { + // Visual feedback: Dim the handle to 40% and turn the triangle icon gray + [_draggingMarker setAlphaValue:0.4]; + [[_draggingMarker label] setTextColor:[CPColor grayColor]]; + } + else + { + // Restore standard styling when dragged back into the active strip + [_draggingMarker setAlphaValue:1.0]; + [[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]]; + } + // Notify the CPTextView that the marker coordinates shifted var client = [self clientView]; if (client && [client respondsToSelector:@selector(rulerView:didMoveMarker:)]) @@ -266,11 +283,16 @@ CPRulerOrientationVertical = 1 [self removeMarker:_draggingMarker]; } + else + { + // Ensure marker style is fully restored if not deleted + [_draggingMarker setAlphaValue:1.0]; + [[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]]; + } _draggingMarker = nil; } - #pragma mark - #pragma mark DOM Layout Builder From c704880e1354e2d6e62a3712160d865d9d4970d8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 29 May 2026 11:41:39 +0200 Subject: [PATCH 28/39] improved: tabstop handles --- AppKit/CPTextView/CPRulerView.j | 134 ++++++++++++++++++++++++++++--- AppKit/CPTextView/CPTextView.j | 41 +++++++++- AppKit/CPTextView/CPTypesetter.j | 67 +++++++++++++++- 3 files changed, 227 insertions(+), 15 deletions(-) diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index dc69a131c..71c9e9992 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -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 diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index c3b14a058..916549d58 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -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) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 186ea0a63..3c209b4fc 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -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 = []; From 7b6cb51f4d833a908ec7c57c54f18b2ec1b46c75 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 29 May 2026 11:45:17 +0200 Subject: [PATCH 29/39] fixed: format menu --- Tests/Manual/CPTextView/AppController.j | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 43165c3d7..128eb6c23 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -257,13 +257,23 @@ [editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"]; [mainMenu setSubmenu:editMenu forItem:item]; + item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0]; + // Format Menu item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0]; var formatMenu = [[CPMenu alloc] initWithTitle:@"Format Menu"]; - [formatMenu addItemWithTitle:@"Font panel" action:@selector(orderFrontFontPanel:) keyEquivalent:@"f"]; + + [formatMenu addItemWithTitle:@"Font panel" action:@selector(orderFrontFontPanel:) keyEquivalent:@"t"]; + [formatMenu addItem:[CPMenuItem separatorItem]]; + // Styles + [formatMenu addItemWithTitle:@"Bold" action:@selector(bold:) keyEquivalent:@"b"]; + [formatMenu addItemWithTitle:@"Italic" action:@selector(italic:) keyEquivalent:@"i"]; [formatMenu addItemWithTitle:@"Underline" action:@selector(underline:) keyEquivalent:@"u"]; + [formatMenu addItem:[CPMenuItem separatorItem]]; + // Alignment [formatMenu addItemWithTitle:@"Align Left" action:@selector(alignLeft:) keyEquivalent:@"{"]; - [formatMenu addItemWithTitle:@"Align Center" action:@selector(alignCenter:) keyEquivalent:@"|"]; + [formatMenu addItemWithTitle:@"Center" action:@selector(alignCenter:) keyEquivalent:@"|"]; [formatMenu addItemWithTitle:@"Align Right" action:@selector(alignRight:) keyEquivalent:@"}"]; + [formatMenu addItemWithTitle:@"Justify" action:@selector(alignJustified:) keyEquivalent:@""]; [mainMenu setSubmenu:formatMenu forItem:item]; // 4. Load Rich Sample Text content From c58a5b520ed933ca900b3d0f24e288a269defa45 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 29 May 2026 18:19:03 +0200 Subject: [PATCH 30/39] fixed: tab copypaste --- AppKit/CPTextView/CPParagraphStyle.j | 8 ++++++-- AppKit/CPTextView/CPTextView.j | 3 ++- Tests/Manual/CPTextView/AppController.j | 9 +-------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/AppKit/CPTextView/CPParagraphStyle.j b/AppKit/CPTextView/CPParagraphStyle.j index 2e2f3f6b9..8f7176550 100644 --- a/AppKit/CPTextView/CPParagraphStyle.j +++ b/AppKit/CPTextView/CPParagraphStyle.j @@ -56,6 +56,12 @@ var kDefaultTabInterval = 28.0; return [self initWithTextAlignment:aType location:aLocation options:nil]; } +// Added to resolve the unrecognized selector exception in the RTF producer +- (CPTabStopType)tabStopType +{ + return _alignment; +} + - (BOOL)isEqual:(id)other { if (self === other) return YES; @@ -217,8 +223,6 @@ var _sharedDefaultParagraphStyle = nil; @end -// MARK: - CPMutableParagraphStyle Implementation - // MARK: - CPMutableParagraphStyle Implementation @implementation CPMutableParagraphStyle : CPParagraphStyle diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 916549d58..21ffa7bc3 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -3049,8 +3049,9 @@ var _CPCopyPlaceholder = '-'; // Intercept problematic keys before the browser acts. _CPNativeInputField.addEventListener('keydown', function(e) { - if (e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) { + if (e.key === 'Tab' || e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) { // Prevent browser default action: + // - 'Tab': Prevents browser focus navigation, allowing Cappuccino key bindings (like option-tab or tab) to work. // - 'Enter': Prevents inserting

. // - 'Backspace' on empty: Prevents inserting junk characters on iPadOS. e.preventDefault(); diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 128eb6c23..7abb6d277 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -287,7 +287,7 @@ var elegantForeground = [CPColor colorWithRed:0.18 green:0.24 blue:0.35 alpha:1.0]; // Slate Blue var elegantBackground = [CPColor colorWithRed:0.92 green:0.95 blue:0.98 alpha:1.0]; // Soft Sky Blue tint - [_textView insertText:[[CPAttributedString alloc] initWithString:@"Fusce\n" + [_textView insertText:[[CPAttributedString alloc] initWithString:@"My Headline with blue background\n" attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:@"Arial" size:18], elegantForeground, elegantBackground] forKeys:[CPParagraphStyleAttributeName, CPFontAttributeName, CPForegroundColorAttributeName, CPBackgroundColorAttributeName]]]]; @@ -323,13 +323,6 @@ [theWindow orderFront:self]; [CPMenu setMenuBarVisible:YES]; - - console.log([[CPFont systemFontOfSize:12] cssString]) - var context = document.createElement("canvas").getContext("2d"); - context.font = '12px Arial, sans-serif'; - var testingText = 'A A A A A A A A'; - console.log(ROUND(context.measureText(testingText).width)); - console.log(ROUND([CPPlatformString sizeOfString:testingText withFont:[CPFont systemFontOfSize:12] forWidth:NULL].width)); } - (void)makeRTF:(id)sender From 10bb6723f3405acac579268cf3fbd6271febd0c4 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 29 May 2026 20:02:13 +0200 Subject: [PATCH 31/39] fixed: missing global symbols --- AppKit/CPTextView/CPParagraphStyle.j | 48 ++++++++++++++++++++++++++++ AppKit/CPTextView/CPTextView.j | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTextView/CPParagraphStyle.j b/AppKit/CPTextView/CPParagraphStyle.j index 8f7176550..e89cda17a 100644 --- a/AppKit/CPTextView/CPParagraphStyle.j +++ b/AppKit/CPTextView/CPParagraphStyle.j @@ -26,6 +26,12 @@ CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName"; +// Define missing global tab stop type constants +CPLeftTabStopType = 0; +CPRightTabStopType = 1; +CPCenterTabStopType = 2; +CPDecimalTabStopType = 3; + // Standard Tab Interval (28pts is roughly 4 spaces in standard fonts) var kDefaultTabInterval = 28.0; @@ -203,6 +209,48 @@ var _sharedDefaultParagraphStyle = nil; return [[CPMutableParagraphStyle alloc] initWithParagraphStyle:self]; } +// MARK: - Coding Support + +- (id)initWithCoder:(CPCoder)aCoder +{ + if (self = [super init]) + { + _lineSpacing = [aCoder decodeFloatForKey:@"CPParagraphStyleLineSpacing"]; + _paragraphSpacing = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacing"]; + _alignment = [aCoder decodeIntForKey:@"CPParagraphStyleAlignment"]; + _headIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleHeadIndent"]; + _tailIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleTailIndent"]; + _firstLineHeadIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleFirstLineHeadIndent"]; + _minimumLineHeight = [aCoder decodeFloatForKey:@"CPParagraphStyleMinimumLineHeight"]; + _maximumLineHeight = [aCoder decodeFloatForKey:@"CPParagraphStyleMaximumLineHeight"]; + _lineBreakMode = [aCoder decodeIntForKey:@"CPParagraphStyleLineBreakMode"]; + _baseWritingDirection = [aCoder decodeIntForKey:@"CPParagraphStyleBaseWritingDirection"]; + _lineHeightMultiple = [aCoder decodeFloatForKey:@"CPParagraphStyleLineHeightMultiple"]; + _paragraphSpacingBefore = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacingBefore"]; + _defaultTabInterval = [aCoder decodeFloatForKey:@"CPParagraphStyleDefaultTabInterval"]; + _tabStops = [aCoder decodeObjectForKey:@"CPParagraphStyleTabStops"]; + } + return self; +} + +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [aCoder encodeFloat:_lineSpacing forKey:@"CPParagraphStyleLineSpacing"]; + [aCoder encodeFloat:_paragraphSpacing forKey:@"CPParagraphStyleParagraphSpacing"]; + [aCoder encodeInt:_alignment forKey:@"CPParagraphStyleAlignment"]; + [aCoder encodeFloat:_headIndent forKey:@"CPParagraphStyleHeadIndent"]; + [aCoder encodeFloat:_tailIndent forKey:@"CPParagraphStyleTailIndent"]; + [aCoder encodeFloat:_firstLineHeadIndent forKey:@"CPParagraphStyleFirstLineHeadIndent"]; + [aCoder encodeFloat:_minimumLineHeight forKey:@"CPParagraphStyleMinimumLineHeight"]; + [aCoder encodeFloat:_maximumLineHeight forKey:@"CPParagraphStyleMaximumLineHeight"]; + [aCoder encodeInt:_lineBreakMode forKey:@"CPParagraphStyleLineBreakMode"]; + [aCoder encodeInt:_baseWritingDirection forKey:@"CPParagraphStyleBaseWritingDirection"]; + [aCoder encodeFloat:_lineHeightMultiple forKey:@"CPParagraphStyleLineHeightMultiple"]; + [aCoder encodeFloat:_paragraphSpacingBefore forKey:@"CPParagraphStyleParagraphSpacingBefore"]; + [aCoder encodeFloat:_defaultTabInterval forKey:@"CPParagraphStyleDefaultTabInterval"]; + [aCoder encodeObject:_tabStops forKey:@"CPParagraphStyleTabStops"]; +} + // MARK: - Equality - (BOOL)isEqual:(id)other diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 21ffa7bc3..1c1a26091 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -70,7 +70,7 @@ _MidRange = function(a1) function _isWhitespaceCharacter(chr) { - return (chr === '\n' || chr === '\r' || chr === ' ' || chr === '\t'); + return (chr === '\n' || chr === '\r' || chr === ' '); // || chr === '\t' } _characterTripletFromStringAtIndex = function(string, index) From dbc8bfc7f363e37da10949d712ebbc5a13e168f3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 29 May 2026 21:28:16 +0200 Subject: [PATCH 32/39] fixed: pasting tab characters --- AppKit/CPTextView/CPTypesetter.j | 39 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 3c209b4fc..427d75586 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -144,27 +144,25 @@ var CPSystemTypesetterFactory, tabStops = [[CPParagraphStyle defaultParagraphStyle] tabStops]; var l = [tabStops count]; + if (l === 0) return nil; - var lastTab = [tabStops lastObject]; - if (aWidth > [lastTab location]) - return nil; - - for (var i = l - 1; i >= 0; i--) + // Find the first tab stop that is strictly greater than the current width + for (var i = 0; i < l; i++) { var tab = [tabStops objectAtIndex:i]; - if (aWidth > [tab location]) - { - if (i + 1 < l) - return [tabStops objectAtIndex:i + 1]; - } + + if ([tab location] > aWidth) + return tab; } - if (i === -1) - return [tabStops objectAtIndex:0]; + // If aWidth exceeds the last tab stop, dynamically calculate the next + // tab location using the default tab interval. + var defaultInterval = [_currentParagraph defaultTabInterval] || 28.0; + var nextLocation = CEIL((aWidth + 1.0) / defaultInterval) * defaultInterval; - return nil; + return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation]; } - (BOOL)_flushRange:(CPRange)lineRange @@ -268,6 +266,9 @@ var CPSystemTypesetterFactory, currentParagraphMaximumLineHeight, currentParagraphLineSpacing; + // Track physical line starts to prevent overwriting lineOrigin.x in tab segments + var isStartOfPhysicalLine = YES; + // Track paragraph indents and margins var isFirstLineOfLayout = YES, isFirstLineOfParagraph = YES, @@ -308,9 +309,8 @@ var CPSystemTypesetterFactory, 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 we are at the start of a physical line, we update lineOrigin.x + if (isStartOfPhysicalLine) { if (glyphIndex > 0) { @@ -377,6 +377,9 @@ var CPSystemTypesetterFactory, lineRange.length++; measuringRange.length++; + // We are processing characters, so we are no longer at the start of a physical line + isStartOfPhysicalLine = NO; + var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor; @@ -461,7 +464,8 @@ var CPSystemTypesetterFactory, { rangeWidth += 28.0; // standard fallback spacer } - } // fallthrough intentional + break; + } case 32: // ' ' wrapRange = CPMakeRangeCopy(lineRange); wrapWidth = rangeWidth; @@ -543,6 +547,7 @@ var CPSystemTypesetterFactory, _lineFragments = []; _lineHeight = 0; _lineBase = ascent; + isStartOfPhysicalLine = YES; } isTabStop = NO; From 3b1c1c0886be11eb0246301590690f12ca2418f8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 29 May 2026 22:08:00 +0200 Subject: [PATCH 33/39] fixed: tab could not be entered using the keyboard --- AppKit/CPTextView/CPTextView.j | 3 ++- AppKit/CPWindow/CPWindow.j | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 1c1a26091..de62ce3d4 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -3049,7 +3049,8 @@ var _CPCopyPlaceholder = '-'; // Intercept problematic keys before the browser acts. _CPNativeInputField.addEventListener('keydown', function(e) { - if (e.key === 'Tab' || e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) { + if (e.key === 'Tab' || e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) + { // Prevent browser default action: // - 'Tab': Prevents browser focus navigation, allowing Cappuccino key bindings (like option-tab or tab) to work. // - 'Enter': Prevents inserting

. diff --git a/AppKit/CPWindow/CPWindow.j b/AppKit/CPWindow/CPWindow.j index 1b233d067..73530fd08 100644 --- a/AppKit/CPWindow/CPWindow.j +++ b/AppKit/CPWindow/CPWindow.j @@ -1951,7 +1951,8 @@ CPTexturedBackgroundWindowMask return [[self firstResponder] keyUp:anEvent]; case CPKeyDown: - if ([anEvent charactersIgnoringModifiers] === CPTabCharacter) + if ([anEvent charactersIgnoringModifiers] === CPTabCharacter && + !([anEvent modifierFlags] & (CPAlternateKeyMask | CPCommandKeyMask))) { if ([anEvent modifierFlags] & CPShiftKeyMask) [self selectPreviousKeyView:self]; From 4fbd50e81f6f6969df728689c158b9dbf92de889 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 30 May 2026 08:44:43 +0200 Subject: [PATCH 34/39] revert: textTabForWidth-"fix" --- AppKit/CPTextView/CPTypesetter.j | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 427d75586..aa7e39128 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -135,36 +135,34 @@ var CPSystemTypesetterFactory, return [_layoutManager textContainers]; } -// Retrieves correct CPTextTab stop accounting for CPArray properties - (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction { var tabStops = [_currentParagraph tabStops]; if (!tabStops) - tabStops = [[CPParagraphStyle defaultParagraphStyle] tabStops]; + tabStops = [CPParagraphStyle _defaultTabStops]; - var l = [tabStops count]; + var l = tabStops.length; - if (l === 0) + if (aWidth > tabStops[l - 1]._location) return nil; - // Find the first tab stop that is strictly greater than the current width - for (var i = 0; i < l; i++) + for (var i = l - 1; i >= 0; i--) { - var tab = [tabStops objectAtIndex:i]; - - if ([tab location] > aWidth) - return tab; + if (aWidth > tabStops[i]._location) + { + if (i + 1 < l) + return tabStops[i + 1]; + } } - // If aWidth exceeds the last tab stop, dynamically calculate the next - // tab location using the default tab interval. - var defaultInterval = [_currentParagraph defaultTabInterval] || 28.0; - var nextLocation = CEIL((aWidth + 1.0) / defaultInterval) * defaultInterval; + if (i === -1) + return tabStops[0]; - return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation]; + return nil; } + - (BOOL)_flushRange:(CPRange)lineRange lineOrigin:(CGPoint)lineOrigin currentContainer:(CPTextContainer)aContainer From 979118a86febca28190a715bb05221f30217693a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 30 May 2026 08:55:25 +0200 Subject: [PATCH 35/39] restored: tabstop-fix --- AppKit/CPTextView/CPTypesetter.j | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index aa7e39128..06c8a368f 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -135,34 +135,36 @@ var CPSystemTypesetterFactory, return [_layoutManager textContainers]; } +// Retrieves correct CPTextTab stop accounting for CPArray properties - (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction { var tabStops = [_currentParagraph tabStops]; if (!tabStops) - tabStops = [CPParagraphStyle _defaultTabStops]; + tabStops = [[CPParagraphStyle defaultParagraphStyle] tabStops]; - var l = tabStops.length; + var l = [tabStops count]; - if (aWidth > tabStops[l - 1]._location) + if (l === 0) return nil; - for (var i = l - 1; i >= 0; i--) + // Find the first tab stop that is strictly greater than the current width + for (var i = 0; i < l; i++) { - if (aWidth > tabStops[i]._location) - { - if (i + 1 < l) - return tabStops[i + 1]; - } + var tab = [tabStops objectAtIndex:i]; + + if ([tab location] > aWidth) + return tab; } - if (i === -1) - return tabStops[0]; + // If aWidth exceeds the last tab stop, dynamically calculate the next + // tab location using the default tab interval. + var defaultInterval = [_currentParagraph defaultTabInterval] || 28.0; + var nextLocation = CEIL((aWidth + 1.0) / defaultInterval) * defaultInterval; - return nil; + return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation]; } - - (BOOL)_flushRange:(CPRange)lineRange lineOrigin:(CGPoint)lineOrigin currentContainer:(CPTextContainer)aContainer From 2a83dcb64a9a7374eafd296b7fdaa583c845f47b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 30 May 2026 09:08:56 +0200 Subject: [PATCH 36/39] fixed: adding new tabstops --- AppKit/CPTextView/CPRulerView.j | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index 71c9e9992..391c390db 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -330,6 +330,11 @@ CPRulerOrientationVertical = 1 representedObject:nil]; [self addMarker:newMarker]; + // Notify the client view (e.g., CPTextView) that a new marker was added + var client = [self clientView]; + if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)]) + [client rulerView:self didAddMarker:newMarker]; + _draggingMarker = newMarker; _dragStartPoint = localPoint; _dragStartLocation = rulerLocation; From 5c7215c455b8197179b9fba733fcbe1d8d38bba1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 30 May 2026 09:25:26 +0200 Subject: [PATCH 37/39] new: apply ruler to current paragraph in case of no selection --- AppKit/CPTextView/CPTextView.j | 116 ++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 29 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index de62ce3d4..32d9c6b88 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2492,8 +2492,19 @@ var compareTabStops = function(obj1, obj2, context) { paragraphStyle = [CPParagraphStyle defaultParagraphStyle], currentAttributes = _typingAttributes; - if (selectedRange.length > 0) - currentAttributes = [_textStorage attributesAtIndex:selectedRange.location effectiveRange:nil]; + // Retrieve active attributes at the cursor position if there is no selection + var textLength = [_textStorage length], + charIndex = selectedRange.location; + + if (textLength > 0) + { + if (charIndex >= textLength) + charIndex = textLength - 1; + if (charIndex < 0) + charIndex = 0; + + currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; + } if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; @@ -2512,17 +2523,23 @@ var compareTabStops = function(obj1, obj2, context) { [mutableStyle setTabStops:tabs]; [marker setRepresentedObject:newTab]; - if (selectedRange.length > 0) + // Find the target text range to modify (selection or containing paragraph) + var targetRange = selectedRange; + if (targetRange.length === 0) + targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; + + if (targetRange.length > 0) { - [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(selectedRange)]; + [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)]; [_layoutManager textStorage:_textStorage edited:0 - range:CPMakeRangeCopy(selectedRange) + range:CPMakeRangeCopy(targetRange) changeInLength:0 - invalidatedRange:CPMakeRangeCopy(selectedRange)]; + invalidatedRange:CPMakeRangeCopy(targetRange)]; } - else + + if (selectedRange.length === 0) { [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; @@ -2538,8 +2555,18 @@ var compareTabStops = function(obj1, obj2, context) { paragraphStyle = [CPParagraphStyle defaultParagraphStyle], currentAttributes = _typingAttributes; - if (selectedRange.length > 0) - currentAttributes = [_textStorage attributesAtIndex:selectedRange.location effectiveRange:nil]; + var textLength = [_textStorage length], + charIndex = selectedRange.location; + + if (textLength > 0) + { + if (charIndex >= textLength) + charIndex = textLength - 1; + if (charIndex < 0) + charIndex = 0; + + currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; + } if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; @@ -2576,17 +2603,23 @@ var compareTabStops = function(obj1, obj2, context) { [mutableStyle setTailIndent:[marker imageValue]]; } - if (selectedRange.length > 0) + // Find the target text range to modify (selection or containing paragraph) + var targetRange = selectedRange; + if (targetRange.length === 0) + targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; + + if (targetRange.length > 0) { - [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(selectedRange)]; + [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)]; [_layoutManager textStorage:_textStorage edited:0 - range:CPMakeRangeCopy(selectedRange) + range:CPMakeRangeCopy(targetRange) changeInLength:0 - invalidatedRange:CPMakeRangeCopy(selectedRange)]; + invalidatedRange:CPMakeRangeCopy(targetRange)]; } - else + + if (selectedRange.length === 0) { [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; @@ -2602,8 +2635,18 @@ var compareTabStops = function(obj1, obj2, context) { paragraphStyle = [CPParagraphStyle defaultParagraphStyle], currentAttributes = _typingAttributes; - if (selectedRange.length > 0) - currentAttributes = [_textStorage attributesAtIndex:selectedRange.location effectiveRange:nil]; + var textLength = [_textStorage length], + charIndex = selectedRange.location; + + if (textLength > 0) + { + if (charIndex >= textLength) + charIndex = textLength - 1; + if (charIndex < 0) + charIndex = 0; + + currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; + } if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; @@ -2619,17 +2662,24 @@ var compareTabStops = function(obj1, obj2, context) { [mutableStyle setTabStops:tabs]; - if (selectedRange.length > 0) + // Find the target text range to modify (selection or containing paragraph) + var targetRange = selectedRange; + + if (targetRange.length === 0) + targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; + + if (targetRange.length > 0) { - [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(selectedRange)]; + [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)]; [_layoutManager textStorage:_textStorage edited:0 - range:CPMakeRangeCopy(selectedRange) + range:CPMakeRangeCopy(targetRange) changeInLength:0 - invalidatedRange:CPMakeRangeCopy(selectedRange)]; + invalidatedRange:CPMakeRangeCopy(targetRange)]; } - else + + if (selectedRange.length === 0) { [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; @@ -2638,14 +2688,16 @@ 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]; + var selectedRange = [self selectedRange], + targetRange = selectedRange; - if (selectedRange.length === 0) + if (targetRange.length === 0) + targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; + + if (targetRange.length === 0) return; - var paragraphStyle = [[self textStorage] attribute:CPParagraphStyleAttributeName atIndex:selectedRange.location effectiveRange:NULL]; + var paragraphStyle = [[self textStorage] attribute:CPParagraphStyleAttributeName atIndex:targetRange.location effectiveRange:NULL]; if (!paragraphStyle) paragraphStyle = [CPParagraphStyle defaultParagraphStyle]; @@ -2661,13 +2713,19 @@ var compareTabStops = function(obj1, obj2, context) { [mutableStyle setTabStops:tabs]; - [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(selectedRange)]; + [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)]; [_layoutManager textStorage:_textStorage edited:0 - range:CPMakeRangeCopy(selectedRange) + range:CPMakeRangeCopy(targetRange) changeInLength:0 - invalidatedRange:CPMakeRangeCopy(selectedRange)]; + invalidatedRange:CPMakeRangeCopy(targetRange)]; + + if (selectedRange.length === 0) + { + [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + } // Force layouts and view canvas update [_layoutManager _validateLayoutAndGlyphs]; From 291be04dc336f247b58d3cc25ba69e4846e9485c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 30 May 2026 09:40:25 +0200 Subject: [PATCH 38/39] improved: rtf parser --- AppKit/CPTextView/_CPRTFParser.j | 125 ++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 18 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 0530d5403..df1569481 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -4,11 +4,6 @@ Copyright (C) 2014 Daniel Boehringer -FIXME: this class should be redone using a 'real' parser - - * all paragraph spacing information is currently not parsed - - * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either @@ -37,6 +32,13 @@ FIXME: this class should be redone using a 'real' parser @global CPFontAttributeName @global CPForegroundColorAttributeName +@global CPBackgroundColorAttributeName +@global CPParagraphStyleAttributeName + +@global CPLeftTabStopType +@global CPRightTabStopType +@global CPCenterTabStopType +@global CPDecimalTabStopType var hexTable = []; @@ -65,6 +67,7 @@ var cp1252Map = { BOOL strikethrough; BOOL script; BOOL _tabChanged; + CPTabStopType _nextTabType; } - (id)init @@ -74,6 +77,7 @@ var cp1252Map = { [self resetFont]; [self resetParagraphStyle]; _range = CPMakeRange(0, 0); + _nextTabType = CPLeftTabStopType; } return self; @@ -83,11 +87,19 @@ var cp1252Map = { { var mynew = [_RTFAttribute new]; - mynew.paragraph = [paragraph copy]; + mynew.paragraph = [paragraph mutableCopy]; mynew.fontName = fontName; + mynew.fontSize = fontSize; + mynew.bold = bold; + mynew.italic = italic; + mynew.underline = underline; + mynew.strikethrough = strikethrough; + mynew.script = script; mynew.fgColour = fgColour; mynew.bgColour = bgColour; mynew.ulColour = ulColour; + mynew._tabChanged = _tabChanged; + mynew._nextTabType = _nextTabType; return mynew; } @@ -143,7 +155,9 @@ var cp1252Map = { - (void)resetParagraphStyle { - paragraph = [[CPParagraphStyle defaultParagraphStyle] copy]; + paragraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; + _tabChanged = NO; + _nextTabType = CPLeftTabStopType; } - (void)resetFont @@ -161,7 +175,7 @@ var cp1252Map = { - (void)addTab:(float)location type:(CPTextTabType)type { - var tab = [[CPTextTab alloc] initWithType:CPLeftTabStopType + var tab = [[CPTextTab alloc] initWithType:type location:location]; if (!_tabChanged) @@ -173,6 +187,8 @@ var cp1252Map = { { [paragraph addTabStop: tab]; } + + _nextTabType = CPLeftTabStopType; } - (CPDictionary)dictionary @@ -184,6 +200,9 @@ var cp1252Map = { if (fgColour) [ret setObject:fgColour forKey:CPForegroundColorAttributeName]; + if (bgColour) + [ret setObject:bgColour forKey:CPBackgroundColorAttributeName]; + return ret; } @end @@ -202,7 +221,7 @@ var kRgsymRtf = { "b" : [ "b", 1, false, kRTFParserType_prop, "propBold"], "ul" : [ "ul", 1, false, kRTFParserType_prop, "propUnderline"], "i" : [ "i", 1, false, kRTFParserType_prop, "propItalic"], - "li" : [ "li", 0, false, kRTFParserType_prop, "propPgnFormat"], +// "li" : [ "li", 0, false, kRTFParserType_prop, "propPgnFormat"], "pgnucltr" : [ "pgnucltr", "pgULtr", true, kRTFParserType_prop, "propPgnFormat"], "pgnlcltr" : [ "pgnlcltr", "pgLLtr", true, kRTFParserType_prop, "propPgnFormat"], "qc" : [ "qc", "justC", true, kRTFParserType_prop, "propJust"], @@ -330,7 +349,11 @@ var kRgsymRtf = { - (BOOL)pushState { - _states.push(_curState); + // Push stack as an object containing scoping context + _states.push({ + curState: _curState, + run: [_currentRun copy] + }); return YES; } @@ -338,7 +361,12 @@ var kRgsymRtf = { { if (_states.length > 0) { - _curState = _states.pop(); + var state = _states.pop(); + _curState = state.curState; + + [self _flushCurrentRun]; + _currentRun = state.run; + _currentRun._range = CPMakeRange([_result length], 0); } return YES; } @@ -412,10 +440,14 @@ var kRgsymRtf = { var dict = [_currentRun dictionary]; [_result setAttributes:dict range:_currentRun._range]; // flush previous run - _currentRun.fgColour = [CPColor blackColor]; + + // Deep copy the current run style for the next sequence of characters + _currentRun = [_currentRun copy]; } else + { _currentRun = [_RTFAttribute new]; + } _currentRun._range = CPMakeRange(newOffset, 0); // open a new one } @@ -428,6 +460,7 @@ var kRgsymRtf = { { case "pard": [self _flushCurrentRun]; + [_currentRun resetParagraphStyle]; break; case "b": // bold @@ -436,7 +469,7 @@ var kRgsymRtf = { if (_currentRun && _currentRun.bold) [self _flushCurrentRun]; - _currentRun.bold = NO + _currentRun.bold = NO; } else { @@ -454,7 +487,7 @@ var kRgsymRtf = { if (_currentRun && _currentRun.italic) [self _flushCurrentRun]; - _currentRun.italic = NO + _currentRun.italic = NO; } else { @@ -470,6 +503,18 @@ var kRgsymRtf = { [_currentRun.paragraph setAlignment:CPCenterTextAlignment]; break; + case "ql": // paragraph left + [_currentRun.paragraph setAlignment:CPLeftTextAlignment]; + break; + + case "qr": // paragraph right + [_currentRun.paragraph setAlignment:CPRightTextAlignment]; + break; + + case "qj": // paragraph justified + [_currentRun.paragraph setAlignment:CPJustifiedTextAlignment]; + break; + case "paperw": _paper.width = param; break; @@ -576,6 +621,20 @@ var kRgsymRtf = { break; + case "cb": // change background color + case "highlight": + [self _flushCurrentRun]; + var colorIndex = parseInt(param) - 1; + + if (_currentRun) + { + if (colorIndex >= 0 && colorIndex < _colorArray.length) + _currentRun.bgColour = _colorArray[colorIndex]; + else + _currentRun.bgColour = nil; + } + break; + case "f": // change font [self _flushCurrentRun]; var fontIndex = parseInt(param); @@ -589,11 +648,41 @@ var kRgsymRtf = { _currentRun.fontSize = parseInt(param) / 2; break; - case "tx": // tabstop + case "fi": // first line indent + if (_currentRun) + [_currentRun.paragraph setFirstLineHeadIndent:parseInt(param) / 20.0]; + break; + + case "li": // left indent / head indent + if (_currentRun) + [_currentRun.paragraph setHeadIndent:parseInt(param) / 20.0]; + break; + + case "ri": // right indent / tail indent + if (_currentRun) + [_currentRun.paragraph setTailIndent:parseInt(param) / 20.0]; + break; + + case "tqc": // center tab stop style flag + if (_currentRun) + _currentRun._nextTabType = CPCenterTabStopType; + break; + + case "tqr": // right tab stop style flag + if (_currentRun) + _currentRun._nextTabType = CPRightTabStopType; + break; + + case "tqdec": // decimal tab stop style flag + if (_currentRun) + _currentRun._nextTabType = CPDecimalTabStopType; + break; + + case "tx": // tabstop location definition var location = parseInt(param) / 20; if (_currentRun) - [_currentRun addTab:location type:CPLeftTabStopType]; + [_currentRun addTab:location type:_currentRun._nextTabType]; break; @@ -739,12 +828,12 @@ var kRgsymRtf = { { var byteVal = parseInt(ch, 16); var unicodeVal = byteVal; - + // Windows-1252 Mapping für den Bereich 0x80 - 0x9F anwenden if (byteVal >= 0x80 && byteVal <= 0x9F) { unicodeVal = cp1252Map[byteVal] || byteVal; } - + [self _appendPlainString: String.fromCharCode(unicodeVal)]; } _hexreturn = NO; From 22fdc971c7da4a7fafafae278a1d6ac0a7679f47 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 30 May 2026 10:11:00 +0200 Subject: [PATCH 39/39] improved: marker icons --- AppKit/CPTextView/CPRulerView.j | 64 ++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index 391c390db..d72e9803a 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -79,6 +79,7 @@ CPRulerOrientationVertical = 1 } // Dynamically sets the Unicode triangle direction based on the alignment or indent type +// Dynamically sets the Unicode arrow direction/type based on the alignment or indent type - (void)updateMarkerIcon { if ([_representedObject isKindOfClass:[CPTextTab class]]) @@ -91,12 +92,22 @@ CPRulerOrientationVertical = 1 else if (align === CPRightTextAlignment) [_label setStringValue:@"◀"]; // Right-aligned points Left } + else if ([_representedObject isKindOfClass:[CPString class]]) + { + if (_representedObject === @"CPFirstLineIndent") + [_label setStringValue:@"⥔"]; // Dotted shaft arrow pointing down for first-line indent + else if (_representedObject === @"CPHeadIndent") + [_label setStringValue:@"⥜"]; // Solid shaft arrow pointing down for following-lines (head) indent + else if (_representedObject === @"CPTailIndent") + [_label setStringValue:@"⥘"]; // Solid downward triangle for tail indent + else + [_label setStringValue:@"⇡"]; // Fallback standard up marker + } else { - [_label setStringValue:@"▲"]; // Indent markers point Up + [_label setStringValue:@"⇡"]; // Fallback standard up marker } } - #pragma mark - #pragma mark Context Menu Support @@ -288,6 +299,12 @@ CPRulerOrientationVertical = 1 var x = markerLocation - scrollPoint.x - 6.0, // Center the 12px wide marker y = rulerHeight - 11.0; // Sit perfectly above bottom border + // Keep horizontal marker within the bounds of the ruler to prevent clipping + if (x < 0.0) + x = 0.0; + else if (x + 12.0 > rulerWidth) + x = rulerWidth - 12.0; + [aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)]; } else @@ -295,6 +312,12 @@ CPRulerOrientationVertical = 1 var x = rulerWidth - 11.0, y = markerLocation - scrollPoint.y - 6.0; + // Keep vertical marker within the bounds of the ruler to prevent clipping + if (y < 0.0) + y = 0.0; + else if (y + 12.0 > rulerHeight) + y = rulerHeight - 12.0; + [aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)]; } } @@ -433,10 +456,11 @@ CPRulerOrientationVertical = 1 { var start = Math.floor(scrollPoint.x / 10) * 10, end = scrollPoint.x + visibleSize.width, - rulerHeight = CGRectGetHeight([self bounds]); + rulerHeight = CGRectGetHeight([self bounds]), + rulerWidth = CGRectGetWidth([self bounds]); // Draw solid horizontal bottom border (pure, razor-sharp CSS DOM view) - var bottomBorder = [[CPView alloc] initWithFrame:CGRectMake(0, rulerHeight - 1, visibleSize.width, 1)]; + var bottomBorder = [[CPView alloc] initWithFrame:CGRectMake(0, rulerHeight - 1, rulerWidth, 1)]; [bottomBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; [self addSubview:bottomBorder]; @@ -457,11 +481,26 @@ CPRulerOrientationVertical = 1 // Unit label if (isMajor) { - var label = [[CPTextField alloc] initWithFrame:CGRectMake(screenX - 20.0, 1.0, 40.0, 12.0)]; + var labelX = screenX - 20.0, + alignment = CPCenterTextAlignment; + + // Adjust label frame and alignment if it lands near left/right bounds + if (labelX < 0.0) + { + labelX = Math.max(0.0, screenX); + alignment = CPLeftTextAlignment; + } + else if (labelX + 40.0 > rulerWidth) + { + labelX = rulerWidth - 40.0; + alignment = CPRightTextAlignment; + } + + var label = [[CPTextField alloc] initWithFrame:CGRectMake(labelX, 1.0, 40.0, 12.0)]; [label setStringValue:[CPString stringWithFormat:@"%d", val]]; [label setFont:[CPFont systemFontOfSize:8.0]]; [label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]]; - [label setAlignment:CPCenterTextAlignment]; + [label setAlignment:alignment]; [self addSubview:label]; } } @@ -471,10 +510,11 @@ CPRulerOrientationVertical = 1 // Vertical Ruler var start = Math.floor(scrollPoint.y / 10) * 10, end = scrollPoint.y + visibleSize.height, + rulerHeight = CGRectGetHeight([self bounds]), rulerWidth = CGRectGetWidth([self bounds]); // Draw solid vertical right border (pure DOM) - var rightBorder = [[CPView alloc] initWithFrame:CGRectMake(rulerWidth - 1, 0, 1, visibleSize.height)]; + var rightBorder = [[CPView alloc] initWithFrame:CGRectMake(rulerWidth - 1, 0, 1, rulerHeight)]; [rightBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; [self addSubview:rightBorder]; @@ -495,7 +535,15 @@ CPRulerOrientationVertical = 1 // Unit label if (isMajor) { - var label = [[CPTextField alloc] initWithFrame:CGRectMake(1.0, screenY - 6.0, rulerWidth - 12.0, 12.0)]; + var labelY = screenY - 6.0; + + // Adjust label frame if it lands near top/bottom bounds + if (labelY < 0.0) + labelY = 0.0; + else if (labelY + 12.0 > rulerHeight) + labelY = rulerHeight - 12.0; + + var label = [[CPTextField alloc] initWithFrame:CGRectMake(1.0, labelY, rulerWidth - 12.0, 12.0)]; [label setStringValue:[CPString stringWithFormat:@"%d", val]]; [label setFont:[CPFont systemFontOfSize:8.0]]; [label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]];