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];