diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index 67b977613..4699e61bb 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -20,725 +20,1022 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +@import +@import @import -@import "CPView.j" -@import "CPTextField.j" + +@import "CGContext.j" +@import "CGGeometry.j" @import "CPColor.j" @import "CPFont.j" -@import "CPMenu.j" -@import "CPMenuItem.j" +@import "CPGraphicsContext.j" +@import "CPImage.j" +@import "CPRulerMarker.j" +@import "CPView.j" + +@class CPMenu +@class CPScrollView @typedef CPRulerOrientation CPHorizontalRuler = 0; CPVerticalRuler = 1; -CPRulerOrientationHorizontal = 0; -CPRulerOrientationVertical = 1; +CPRulerOrientationHorizontal = CPHorizontalRuler; +CPRulerOrientationVertical = CPVerticalRuler; -@class CPRulerView; -@class CPScrollView; -@class CPTextTab; +/*! + Names of the measurement units that every CPRulerView supports. +*/ +CPRulerViewUnitInches = @"Inches"; +CPRulerViewUnitCentimeters = @"Centimeters"; +CPRulerViewUnitPoints = @"Points"; +CPRulerViewUnitPicas = @"Picas"; +/* + The registered measurement units, by name. Each entry holds the abbreviation, the number + of points in one unit, and the cycles used to space labels and hash marks. +*/ +var CPRulerViewRegisteredUnits = @{}; -// MARK: - CPRulerMarker +/* + The smallest distances, in points, kept between two labels and between two hash marks. +*/ +var CPRulerViewMinimumLabelSpacing = 40.0, + CPRulerViewMinimumHashMarkSpacing = 5.0; -@implementation CPRulerMarker : CPView -{ - CPRulerView _rulerView @accessors(property=rulerView); - float _imageValue @accessors(property=imageValue); - id _representedObject @accessors(property=representedObject); - CPTextField _label; - CPView _customHandleView; -} +/*! + @ingroup appkit + @class CPRulerView -- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation imageValue:(float)anImageValue representedObject:(id)anObject -{ - if (self = [super initWithFrame:CGRectMake(0, 0, 12, 12)]) - { - _rulerView = aRulerView; - _imageValue = anImageValue; - _representedObject = anObject; - - // CRITICAL: Subviews must not intercept mouse events so CPRulerView receives all mouseDragged: events - [self setHitTests:NO]; + A CPRulerView shows a ruler along the horizontal or vertical edge of a CPScrollView's + content, with hash marks and labels in a chosen measurement unit, and markers such as + tab stops and paragraph indents. - _label = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 12, 12)]; - [_label setFont:[CPFont systemFontOfSize:10.0]]; - [_label setAlignment:CPCenterTextAlignment]; - [_label setHitTests:NO]; - [self addSubview:_label]; - - _customHandleView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; - [_customHandleView setHitTests:NO]; - [self addSubview:_customHandleView]; - - // Inherit HUD state from ruler if present - if ([aRulerView hasThemeState:CPThemeStateHUD]) - [self setThemeState:CPThemeStateHUD]; + The ruler is divided across its thickness into three areas: an optional accessory view, + the marker area, and the rule area with the hash marks and labels, which lies next to + the scroll view's content. The baseline separates the marker area from the rule area. - [self updateMarkerIcon]; - } - return self; -} - -- (BOOL)isHUD -{ - return [self hasThemeState:CPThemeStateHUD] || (_rulerView && [_rulerView hasThemeState:CPThemeStateHUD]); -} - -- (void)setThemeState:(CPThemeState)aState -{ - [super setThemeState:aState]; - [self updateMarkerIcon]; -} - -- (void)unsetThemeState:(CPThemeState)aState -{ - [super unsetThemeState:aState]; - [self updateMarkerIcon]; -} - -- (CPTextField)label -{ - return _label; -} - -- (void)setRepresentedObject:(id)anObject -{ - _representedObject = anObject; - [self updateMarkerIcon]; -} - -- (void)setFrame:(CGRect)aFrame -{ - [super setFrame:aFrame]; - [self updateMarkerIcon]; -} - -- (void)updateMarkerIcon -{ - var isHUD = [self isHUD], - isIndentMarker = (_representedObject === @"CPFirstLineIndent" || _representedObject === @"CPHeadIndent"); - - if (isIndentMarker) - { - [_label setHidden:YES]; - [_customHandleView setHidden:NO]; - - var frame = [self bounds]; - [_customHandleView setFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; - [[_customHandleView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; - - var isFirstLine = (_representedObject === @"CPFirstLineIndent"); - [_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.45 alpha:1.0]]; - - // Border outline - if (isHUD) - [_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.12 alpha:1.0]]; - else - [_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.5 alpha:1.0]]; - - // Inner fill - var innerView = [[CPView alloc] initWithFrame:CGRectMake(1.0, 1.0, frame.size.width - 2.0, frame.size.height - 2.0)]; - - if (isHUD) - { - if (isFirstLine) - [innerView setBackgroundColor:[CPColor colorWithWhite:0.38 alpha:1.0]]; - else - [innerView setBackgroundColor:[CPColor colorWithWhite:0.28 alpha:1.0]]; - } - else - { - if (isFirstLine) - [innerView setBackgroundColor:[CPColor colorWithWhite:0.92 alpha:1.0]]; - else - [innerView setBackgroundColor:[CPColor colorWithWhite:0.80 alpha:1.0]]; - } - - [_customHandleView addSubview:innerView]; - - // Horizontal indicator grip line - var gripLine = [[CPView alloc] initWithFrame:CGRectMake(Math.floor(frame.size.width / 2.0) - 1.0, 2.0, 1.0, frame.size.height - 4.0)]; - if (isHUD) - [gripLine setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]]; - else - [gripLine setBackgroundColor:[CPColor colorWithWhite:0.60 alpha:1.0]]; - - [innerView addSubview:gripLine]; - } - else - { - [_label setHidden:NO]; - [_customHandleView setHidden:YES]; - [_label setFrame:[self bounds]]; - - // Label color adapted to HUD state - if (isHUD) - [_label setTextColor:[CPColor colorWithWhite:0.92 alpha:1.0]]; - else - [_label setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]]; - - if ([_representedObject isKindOfClass:[CPTextTab class]]) - { - var align = [_representedObject alignment]; - if (align === CPLeftTextAlignment) - [_label setStringValue:@"▶"]; - else if (align === CPCenterTextAlignment) - [_label setStringValue:@"▼"]; - else if (align === CPRightTextAlignment) - [_label setStringValue:@"◀"]; - } - else - { - [_label setStringValue:@"▲"]; - } - } -} - -- (CPMenu)menuForEvent:(CPEvent)anEvent -{ - var menu = [[CPMenu alloc] initWithTitle:@"Marker Context Menu"]; - - 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]]; - } - - var deleteTitle = @"Delete Tab Stop"; - if (_representedObject === @"CPFirstLineIndent") - deleteTitle = @"Reset 1st line indentation"; - else if (_representedObject === @"CPHeadIndent") - deleteTitle = @"Reset head indentation"; - - 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, - newTab = [[CPTextTab alloc] initWithType:alignment location:_imageValue]; - - [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 - - -// MARK: - CPRulerView + A ruler works for a client view, usually the scroll view's document view. Marker + locations are expressed in the client view's coordinate system. The ruler asks the + client view before and after the user adds, moves or removes a marker, through these + optional client methods: +
+    - (BOOL)rulerView:(CPRulerView)aRulerView shouldAddMarker:(CPRulerMarker)aMarker;
+    - (float)rulerView:(CPRulerView)aRulerView willAddMarker:(CPRulerMarker)aMarker atLocation:(float)aLocation;
+    - (void)rulerView:(CPRulerView)aRulerView didAddMarker:(CPRulerMarker)aMarker;
+    - (BOOL)rulerView:(CPRulerView)aRulerView shouldMoveMarker:(CPRulerMarker)aMarker;
+    - (float)rulerView:(CPRulerView)aRulerView willMoveMarker:(CPRulerMarker)aMarker toLocation:(float)aLocation;
+    - (void)rulerView:(CPRulerView)aRulerView didMoveMarker:(CPRulerMarker)aMarker;
+    - (BOOL)rulerView:(CPRulerView)aRulerView shouldRemoveMarker:(CPRulerMarker)aMarker;
+    - (void)rulerView:(CPRulerView)aRulerView didRemoveMarker:(CPRulerMarker)aMarker;
+    - (void)rulerView:(CPRulerView)aRulerView handleMouseDown:(CPEvent)anEvent;
+    - (void)rulerView:(CPRulerView)aRulerView willSetClientView:(CPView)aNewClient;
+    - (float)rulerView:(CPRulerView)aRulerView locationForPoint:(CGPoint)aPoint;
+    - (CGPoint)rulerView:(CPRulerView)aRulerView pointForLocation:(float)aLocation;
+    
+*/ @implementation CPRulerView : CPView { - CPScrollView _scrollView @accessors(property=scrollView); - CPRulerOrientation _orientation @accessors(property=orientation); - + CPScrollView _scrollView; + CPRulerOrientation _orientation; CPView _clientView; - - float _ruleThickness @accessors(property=ruleThickness); - float _reservedThicknessForMarkers; + CPView _accessoryView; CPArray _markers; + CPRulerMarker _trackedMarker; + CPArray _rulerlineLocations; - CPRulerMarker _draggingMarker @accessors(getter=draggingMarker); - CGPoint _dragStartPoint; - float _dragStartLocation; + float _ruleThickness; + float _reservedThicknessForMarkers; + float _reservedThicknessForAccessoryView; + float _originOffset; + CPString _measurementUnits; + + float _labelStep; + CPArray _hashMarkSteps; } ++ (void)initialize +{ + if (self !== [CPRulerView class]) + return; + + [self registerUnitWithName:CPRulerViewUnitInches abbreviation:@"in" unitToPointsConversionFactor:72.0 stepUpCycle:[2.0] stepDownCycle:[0.5]]; + [self registerUnitWithName:CPRulerViewUnitCentimeters abbreviation:@"cm" unitToPointsConversionFactor:28.35 stepUpCycle:[2.0] stepDownCycle:[0.5, 0.2]]; + [self registerUnitWithName:CPRulerViewUnitPoints abbreviation:@"pt" unitToPointsConversionFactor:1.0 stepUpCycle:[10.0] stepDownCycle:[0.5]]; + [self registerUnitWithName:CPRulerViewUnitPicas abbreviation:@"pc" unitToPointsConversionFactor:12.0 stepUpCycle:[10.0] stepDownCycle:[0.5]]; +} + +// MARK: Altering Measurement Units + +/*! + Registers a measurement unit that every ruler view can then use. + + The cycles control the spacing of labels and hash marks. When labels one unit apart + would be too close, the label step is multiplied by the factors of \c upCycle in turn + until they fit. Hash marks between labels divide the label step by the factors of + \c downCycle in turn, for as long as they stay far enough apart. + @param aName the unit's name + @param anAbbreviation the unit's abbreviation + @param aFactor the number of points in one unit + @param upCycle the factors, each greater than 1, that enlarge the label step + @param downCycle the factors, each less than 1, that subdivide the label step +*/ ++ (void)registerUnitWithName:(CPString)aName abbreviation:(CPString)anAbbreviation unitToPointsConversionFactor:(float)aFactor stepUpCycle:(CPArray)upCycle stepDownCycle:(CPArray)downCycle +{ + [CPRulerViewRegisteredUnits setObject:{ abbreviation: anAbbreviation, factor: aFactor, upCycle: upCycle, downCycle: downCycle } forKey:aName]; +} + +/* @ignore */ ++ (CPString)_defaultMeasurementUnits +{ + // As in Cocoa, the default follows the user's region: inches where the imperial system is in common use. + var language = (typeof navigator !== "undefined" && navigator.language) || @"", + region = language.split("-")[1]; + + return (region === @"US" || region === @"LR" || region === @"MM") ? CPRulerViewUnitInches : CPRulerViewUnitCentimeters; +} + +// MARK: Creating a Ruler View + +/*! + Initializes a ruler view for \c aScrollView with the given orientation. The scroll view + is not retained. The ruler's client view is the scroll view's document view until + another is set. + @param aScrollView the scroll view that owns the ruler + @param anOrientation \c CPHorizontalRuler or \c CPVerticalRuler + @return the initialized ruler view +*/ - (id)initWithScrollView:(CPScrollView)aScrollView orientation:(CPRulerOrientation)anOrientation { - if (self = [super initWithFrame:CGRectMakeZero()]) + self = [super initWithFrame:CGRectMakeZero()]; + + if (self) { _scrollView = aScrollView; _orientation = anOrientation; - _clientView = [aScrollView documentView]; - - _ruleThickness = (anOrientation === CPHorizontalRuler) ? 16.0 : 24.0; - _reservedThicknessForMarkers = 0.0; _markers = []; - - [self setBackgroundColor:[CPColor colorWithWhite:0.96 alpha:1.0]]; + _rulerlineLocations = []; + + _ruleThickness = 16.0; + _reservedThicknessForMarkers = anOrientation === CPHorizontalRuler ? 15.0 : 0.0; + _reservedThicknessForAccessoryView = 0.0; + _originOffset = 0.0; + _measurementUnits = [[self class] _defaultMeasurementUnits]; + + [self invalidateHashMarks]; } + return self; } -- (BOOL)isHUD +// MARK: Ruler Layout + +/*! + Returns the scroll view that owns the ruler. +*/ +- (CPScrollView)scrollView { - return [self hasThemeState:CPThemeStateHUD]; + return _scrollView; } -- (void)setThemeState:(CPThemeState)aState +/*! + Sets the scroll view that owns the ruler. The scroll view is not retained. + @param aScrollView the owning scroll view +*/ +- (void)setScrollView:(CPScrollView)aScrollView { - [super setThemeState:aState]; - for (var i = 0; i < [_markers count]; i++) - [[_markers objectAtIndex:i] setThemeState:aState]; - [self updateRuler]; + _scrollView = aScrollView; } -- (void)unsetThemeState:(CPThemeState)aState +/*! + Returns the ruler's orientation: \c CPHorizontalRuler or \c CPVerticalRuler. +*/ +- (CPRulerOrientation)orientation { - [super unsetThemeState:aState]; - for (var i = 0; i < [_markers count]; i++) - [[_markers objectAtIndex:i] unsetThemeState:aState]; - [self updateRuler]; + return _orientation; } -- (void)setFrame:(CGRect)aFrame +/*! + Sets the ruler's orientation. + @param anOrientation \c CPHorizontalRuler or \c CPVerticalRuler +*/ +- (void)setOrientation:(CPRulerOrientation)anOrientation { - [super setFrame:aFrame]; - [self updateRuler]; + if (_orientation === anOrientation) + return; + + _orientation = anOrientation; + [self _noteThicknessChanged]; } +/*! + Returns the thickness of the rule area, where hash marks and labels are drawn. +*/ +- (float)ruleThickness +{ + return _ruleThickness; +} + +/*! + Sets the thickness of the rule area. + @param aThickness the new thickness +*/ +- (void)setRuleThickness:(float)aThickness +{ + if (_ruleThickness === aThickness) + return; + + _ruleThickness = aThickness; + [self _noteThicknessChanged]; +} + +/*! + Returns the thickness reserved for markers, between the accessory view and the rule area. +*/ +- (float)reservedThicknessForMarkers +{ + return _reservedThicknessForMarkers; +} + +/*! + Sets the thickness reserved for markers. A client view usually sets it to the largest + \c thicknessRequiredInRuler of the markers it uses. + @param aThickness the new thickness +*/ +- (void)setReservedThicknessForMarkers:(float)aThickness +{ + if (_reservedThicknessForMarkers === aThickness) + return; + + _reservedThicknessForMarkers = aThickness; + [self _noteThicknessChanged]; +} + +/*! + Returns the thickness reserved for the accessory view. +*/ +- (float)reservedThicknessForAccessoryView +{ + return _reservedThicknessForAccessoryView; +} + +/*! + Sets the thickness reserved for the accessory view. + @param aThickness the new thickness +*/ +- (void)setReservedThicknessForAccessoryView:(float)aThickness +{ + if (_reservedThicknessForAccessoryView === aThickness) + return; + + _reservedThicknessForAccessoryView = aThickness; + [self _noteThicknessChanged]; +} + +/*! + Returns the total thickness the ruler needs: the accessory view, the markers and the + rule area together. +*/ +- (float)requiredThickness +{ + var accessoryThickness = _reservedThicknessForAccessoryView; + + if (_accessoryView) + { + var accessorySize = [_accessoryView frameSize]; + accessoryThickness = MAX(accessoryThickness, _orientation === CPHorizontalRuler ? accessorySize.height : accessorySize.width); + } + + return accessoryThickness + _reservedThicknessForMarkers + _ruleThickness; +} + +/*! + Returns the location of the baseline, which separates the marker area from the rule + area, in the ruler's coordinate system. +*/ +- (float)baselineLocation +{ + return [self requiredThickness] - _ruleThickness; +} + +/*! + Returns \c YES. The ruler's coordinate system has its origin at the top left. +*/ +- (BOOL)isFlipped +{ + return YES; +} + +/* @ignore */ +- (void)_noteThicknessChanged +{ + [_scrollView tile]; + [self setNeedsDisplay:YES]; +} + +// MARK: Setting the Client View + +/*! + Returns the ruler's client view, or the scroll view's document view if no client view + has been set. +*/ - (CPView)clientView { return _clientView || [_scrollView documentView]; } +/*! + Sets the ruler's client view. The current client view is told first, through + \c rulerView:willSetClientView:, and the ruler's markers are removed. + @param aView the new client view +*/ - (void)setClientView:(CPView)aView { - _clientView = aView; -} - -- (void)addMarker:(CPRulerMarker)aMarker -{ - if ([_markers containsObject:aMarker]) + if (_clientView === aView) return; - - if ([self hasThemeState:CPThemeStateHUD]) - [aMarker setThemeState:CPThemeStateHUD]; - [_markers addObject:aMarker]; - [self addSubview:aMarker]; - [self _positionMarker:aMarker]; + var currentClient = [self clientView]; + + if ([currentClient respondsToSelector:@selector(rulerView:willSetClientView:)]) + [currentClient rulerView:self willSetClientView:aView]; + + _clientView = aView; + [self setMarkers:nil]; } -- (void)removeMarker:(CPRulerMarker)aMarker +// MARK: Setting an Accessory View + +/*! + Returns the ruler's accessory view. +*/ +- (CPView)accessoryView { - [_markers removeObject:aMarker]; - [aMarker removeFromSuperview]; + return _accessoryView; } +/*! + Sets the ruler's accessory view, which is shown in the area on the far side of the + markers from the rule area. + @param aView the new accessory view +*/ +- (void)setAccessoryView:(CPView)aView +{ + if (_accessoryView === aView) + return; + + [_accessoryView removeFromSuperview]; + _accessoryView = aView; + + if (_accessoryView) + { + [_accessoryView setFrameOrigin:CGPointMakeZero()]; + [self addSubview:_accessoryView]; + } + + [self _noteThicknessChanged]; +} + +// MARK: Setting the Zero Mark Position + +/*! + Returns the distance from the origin of the scroll view's document view to the ruler's + zero hash mark. +*/ +- (float)originOffset +{ + return _originOffset; +} + +/*! + Sets the distance from the origin of the scroll view's document view to the ruler's + zero hash mark. A text view sets it to the start of its text, so that the ruler's + zero lines up with the left edge of the text. + @param anOffset the new offset +*/ +- (void)setOriginOffset:(float)anOffset +{ + if (_originOffset === anOffset) + return; + + _originOffset = anOffset; + [self setNeedsDisplay:YES]; +} + +// MARK: Measurement Units + +/*! + Returns the name of the measurement unit the ruler shows. +*/ +- (CPString)measurementUnits +{ + return _measurementUnits; +} + +/*! + Sets the measurement unit the ruler shows. The unit must have been registered. + @param aUnitName the name of a registered unit +*/ +- (void)setMeasurementUnits:(CPString)aUnitName +{ + if (_measurementUnits === aUnitName || ![CPRulerViewRegisteredUnits objectForKey:aUnitName]) + return; + + _measurementUnits = aUnitName; + [self invalidateHashMarks]; +} + +/*! + Discards the cached spacing of labels and hash marks, so that it is computed again the + next time the ruler is drawn. +*/ +- (void)invalidateHashMarks +{ + _labelStep = nil; + _hashMarkSteps = nil; + [self setNeedsDisplay:YES]; +} + +// MARK: Adding and Removing Markers + +/*! + Returns the ruler's markers. +*/ +- (CPArray)markers +{ + return _markers; +} + +/*! + Replaces the ruler's markers with \c newMarkers, without consulting the client view. + @param newMarkers the new markers, or \c nil to remove all markers +*/ - (void)setMarkers:(CPArray)newMarkers { - for (var i = 0; i < [_markers count]; i++) - [[_markers objectAtIndex:i] removeFromSuperview]; - - _markers = [newMarkers mutableCopy]; - var isHUD = [self hasThemeState:CPThemeStateHUD]; - - for (var i = 0; i < [_markers count]; i++) - { - var marker = [_markers objectAtIndex:i]; - if (isHUD) - [marker setThemeState:CPThemeStateHUD]; - [self addSubview:marker]; - [self _positionMarker:marker]; - } - [self updateRuler]; + _markers = newMarkers ? [newMarkers copy] : []; + [self setNeedsDisplay:YES]; } +/*! + Adds \c aMarker to the ruler, without consulting the client view. + @param aMarker the marker to add +*/ +- (void)addMarker:(CPRulerMarker)aMarker +{ + if ([_markers containsObjectIdenticalTo:aMarker]) + return; + + _markers.push(aMarker); + [self setNeedsDisplay:YES]; +} + +/*! + Removes \c aMarker from the ruler, without consulting the client view. + @param aMarker the marker to remove +*/ +- (void)removeMarker:(CPRulerMarker)aMarker +{ + [_markers removeObjectIdenticalTo:aMarker]; + [self setNeedsDisplay:YES]; +} + +/*! + Lets the user add \c aMarker to the ruler by dragging it, starting with \c anEvent. The + client view is consulted before the marker is added. A client view usually calls this + method from \c rulerView:handleMouseDown:. + @param aMarker the marker to add, which is not yet in the ruler + @param anEvent the mouse-down event + @return \c YES if tracking started +*/ +- (BOOL)trackMarker:(CPRulerMarker)aMarker withMouseEvent:(CPEvent)anEvent +{ + return [aMarker trackMouse:anEvent adding:YES]; +} + +// MARK: Drawing Temporary Ruler Lines + +/*! + Moves a temporary line drawn across the ruler from \c oldLocation to \c newLocation, + both in the client view's coordinate system. Passing -1 as \c oldLocation draws a new + line, and passing -1 as \c newLocation removes the line. + @param oldLocation the line's current location, or -1 + @param newLocation the line's new location, or -1 +*/ +- (void)moveRulerlineFromLocation:(float)oldLocation toLocation:(float)newLocation +{ + if (oldLocation >= 0.0) + [_rulerlineLocations removeObject:oldLocation]; + + if (newLocation >= 0.0) + _rulerlineLocations.push(newLocation); + + [self setNeedsDisplay:YES]; +} + +// MARK: HUD Appearance + +/*! + Returns \c YES if the ruler is drawn with the dark HUD appearance. +*/ +- (BOOL)isHUD +{ + return [self hasThemeState:CPThemeStateHUD]; +} + +/* @ignore */ +- (BOOL)setThemeState:(ThemeState)aState +{ + var result = [super setThemeState:aState]; + [self setNeedsDisplay:YES]; + + return result; +} + +/* @ignore */ +- (BOOL)unsetThemeState:(ThemeState)aState +{ + var result = [super unsetThemeState:aState]; + [self setNeedsDisplay:YES]; + + return result; +} + +/* @ignore */ +- (CPColor)_backgroundColorForDrawing +{ + return [CPColor colorWithWhite:([self isHUD] ? 0.18 : 0.96) alpha:1.0]; +} + +/* @ignore */ +- (CPColor)_edgeColorForDrawing +{ + return [CPColor colorWithWhite:([self isHUD] ? 0.10 : 0.75) alpha:1.0]; +} + +/* @ignore */ +- (CPColor)_hashMarkColorForDrawing +{ + return [CPColor colorWithWhite:([self isHUD] ? 0.60 : 0.55) alpha:1.0]; +} + +/* @ignore */ +- (CPColor)_labelColorForDrawing +{ + return [CPColor colorWithWhite:([self isHUD] ? 0.75 : 0.55) alpha:1.0]; +} + +/* @ignore */ +- (CPColor)_rulerlineColorForDrawing +{ + return [CPColor colorWithWhite:([self isHUD] ? 0.85 : 0.3) alpha:1.0]; +} + +// MARK: Drawing + +/*! + Draws the ruler: its background, hash marks and labels, temporary ruler lines and markers. + @param aRect the rectangle to draw +*/ +- (void)drawRect:(CGRect)aRect +{ + var context = [[CPGraphicsContext currentContext] graphicsPort], + bounds = [self bounds]; + + CGContextSetFillColor(context, [self _backgroundColorForDrawing]); + CGContextFillRect(context, bounds); + + // The edge next to the scroll view's content. + CGContextSetFillColor(context, [self _edgeColorForDrawing]); + + if (_orientation === CPHorizontalRuler) + CGContextFillRect(context, CGRectMake(CGRectGetMinX(bounds), CGRectGetMaxY(bounds) - 1.0, CGRectGetWidth(bounds), 1.0)); + else + CGContextFillRect(context, CGRectMake(CGRectGetMaxX(bounds) - 1.0, CGRectGetMinY(bounds), 1.0, CGRectGetHeight(bounds))); + + [self drawHashMarksAndLabelsInRect:aRect]; + [self _drawRulerlinesInRect:aRect]; + [self drawMarkersInRect:aRect]; +} + +/*! + Draws the hash marks and labels that lie within \c aRect, in the rule area. + @param aRect the rectangle to draw, in the ruler's coordinate system +*/ +- (void)drawHashMarksAndLabelsInRect:(CGRect)aRect +{ + [self _computeHashMarkSpacing]; + + var unit = [CPRulerViewRegisteredUnits objectForKey:_measurementUnits], + context = [[CPGraphicsContext currentContext] graphicsPort], + isHorizontal = _orientation === CPHorizontalRuler, + bounds = [self bounds], + zero = [self _rulerPointForDocumentLocation:_originOffset], + firstPoint = isHorizontal ? CGRectGetMinX(aRect) : CGRectGetMinY(aRect), + lastPoint = isHorizontal ? CGRectGetMaxX(aRect) : CGRectGetMaxY(aRect), + ruleEdge = isHorizontal ? CGRectGetMaxY(bounds) - 1.0 : CGRectGetMaxX(bounds) - 1.0, + smallestStep = _hashMarkSteps.length ? _hashMarkSteps[_hashMarkSteps.length - 1] : _labelStep, + stepPoints = smallestStep * unit.factor, + firstIndex = Math.floor((firstPoint - zero) / stepPoints), + lastIndex = Math.ceil((lastPoint - zero) / stepPoints), + hashMarkColor = [self _hashMarkColorForDrawing], + labelColor = [self _labelColorForDrawing]; + + CGContextSetFillColor(context, hashMarkColor); + CGContextSelectFont(context, [CPFont systemFontOfSize:8.0]); + + for (var index = firstIndex; index <= lastIndex; index++) + { + var value = index * smallestStep, + point = zero + value * unit.factor, + length = [self _hashMarkLengthForValue:value]; + + if (isHorizontal) + CGContextFillRect(context, CGRectMake(ROUND(point), ruleEdge - length, 1.0, length)); + else + CGContextFillRect(context, CGRectMake(ruleEdge - length, ROUND(point), length, 1.0)); + + if (![self _value:value isMultipleOf:_labelStep]) + continue; + + var label = [self _labelForValue:value]; + + CGContextSetFillColor(context, labelColor); + + // Labels sit in the half of the rule area away from the baseline, clear of markers that reach below it. + if (isHorizontal) + CGContextShowTextAtPoint(context, ROUND(point) + 2.0, ruleEdge - 4.5, label); + else + CGContextShowTextAtPoint(context, ruleEdge - _ruleThickness + 1.0, ROUND(point) + 6.0, label); + + CGContextSetFillColor(context, hashMarkColor); + } +} + +/*! + Draws the markers that lie within \c aRect, including a marker the user is adding. + @param aRect the rectangle to draw, in the ruler's coordinate system +*/ +- (void)drawMarkersInRect:(CGRect)aRect +{ + var markers = _trackedMarker ? _markers.concat([_trackedMarker]) : _markers, + count = markers.length; + + for (var i = 0; i < count; i++) + { + var marker = markers[i]; + + [self _redrawWhenImageLoads:[marker image]]; + [marker drawRect:aRect]; + } +} + +/* @ignore */ +- (void)_drawRulerlinesInRect:(CGRect)aRect +{ + var count = _rulerlineLocations.length; + + if (!count) + return; + + var context = [[CPGraphicsContext currentContext] graphicsPort], + bounds = [self bounds]; + + CGContextSetFillColor(context, [self _rulerlineColorForDrawing]); + + for (var i = 0; i < count; i++) + { + var point = ROUND([self _rulerPointForLocation:_rulerlineLocations[i]]); + + if (_orientation === CPHorizontalRuler) + CGContextFillRect(context, CGRectMake(point, CGRectGetMinY(bounds), 1.0, CGRectGetHeight(bounds))); + else + CGContextFillRect(context, CGRectMake(CGRectGetMinX(bounds), point, CGRectGetWidth(bounds), 1.0)); + } +} + +/* @ignore */ +- (void)_redrawWhenImageLoads:(CPImage)anImage +{ + if (!anImage || [anImage loadStatus] === CPImageLoadStatusCompleted) + return; + + [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_markerImageDidLoad:) name:CPImageDidLoadNotification object:anImage]; +} + +/* @ignore */ +- (void)_markerImageDidLoad:(CPNotification)aNotification +{ + [[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:[aNotification object]]; + [self setNeedsDisplay:YES]; +} + +/* @ignore */ +- (void)_computeHashMarkSpacing +{ + if (_labelStep) + return; + + var unit = [CPRulerViewRegisteredUnits objectForKey:_measurementUnits], + labelStep = 1.0, + cycleIndex = 0; + + while (labelStep * unit.factor < CPRulerViewMinimumLabelSpacing) + labelStep *= unit.upCycle[cycleIndex++ % unit.upCycle.length]; + + var steps = [], + step = labelStep; + + cycleIndex = 0; + + while (YES) + { + var nextStep = step * unit.downCycle[cycleIndex++ % unit.downCycle.length]; + + if (nextStep * unit.factor < CPRulerViewMinimumHashMarkSpacing) + break; + + steps.push(nextStep); + step = nextStep; + } + + _labelStep = labelStep; + _hashMarkSteps = steps; +} + +/* @ignore */ +- (float)_hashMarkLengthForValue:(float)aValue +{ + // Hash marks fill only the half of the rule area away from the baseline, like the labels. + var fullLength = ROUND(_ruleThickness / 2.0); + + if ([self _value:aValue isMultipleOf:_labelStep]) + return fullLength; + + // Each finer subdivision of the label step draws a shorter mark. + for (var i = 0; i < _hashMarkSteps.length; i++) + if ([self _value:aValue isMultipleOf:_hashMarkSteps[i]]) + return MAX(2.0, ROUND(fullLength * (0.65 - 0.2 * i))); + + return 2.0; +} + +/* @ignore */ +- (BOOL)_value:(float)aValue isMultipleOf:(float)aStep +{ + var quotient = aValue / aStep; + + return Math.abs(quotient - ROUND(quotient)) < 0.0001; +} + +/* @ignore */ +- (CPString)_labelForValue:(float)aValue +{ + var rounded = ROUND(aValue * 100.0) / 100.0; + + return String(rounded); +} + +// MARK: Coordinate Conversion + +/* @ignore + Converts a location in the client view's coordinate system to a position along the + ruler, in the ruler's coordinate system. +*/ +- (float)_rulerPointForLocation:(float)aLocation +{ + var client = [self clientView]; + + if ([client respondsToSelector:@selector(rulerView:pointForLocation:)]) + { + var clientPoint = [client rulerView:self pointForLocation:aLocation]; + return [self _axisValueOfPoint:[self convertPoint:clientPoint fromView:client]]; + } + + return [self _axisValueOfPoint:[self convertPoint:[self _pointOnAxis:aLocation] fromView:client]]; +} + +/* @ignore + Converts a position along the ruler, in the ruler's coordinate system, to a location in + the client view's coordinate system. +*/ +- (float)_locationForRulerPoint:(float)aPoint +{ + var client = [self clientView], + clientPoint = [self convertPoint:[self _pointOnAxis:aPoint] toView:client]; + + if ([client respondsToSelector:@selector(rulerView:locationForPoint:)]) + return [client rulerView:self locationForPoint:clientPoint]; + + return [self _axisValueOfPoint:clientPoint]; +} + +/* @ignore */ +- (float)_rulerPointForDocumentLocation:(float)aLocation +{ + return [self _axisValueOfPoint:[self convertPoint:[self _pointOnAxis:aLocation] fromView:[_scrollView documentView]]]; +} + +/* @ignore */ +- (CGPoint)_pointOnAxis:(float)aValue +{ + return _orientation === CPHorizontalRuler ? CGPointMake(aValue, 0.0) : CGPointMake(0.0, aValue); +} + +/* @ignore */ +- (float)_axisValueOfPoint:(CGPoint)aPoint +{ + return _orientation === CPHorizontalRuler ? aPoint.x : aPoint.y; +} + +// MARK: Event Handling + +/*! + Starts tracking a marker under the pointer. When no marker is under the pointer, the + client view handles the event through \c rulerView:handleMouseDown:, for example to add + a marker. + @param anEvent the mouse-down event +*/ +- (void)mouseDown:(CPEvent)anEvent +{ + var marker = [self _markerAtPoint:[self convertPoint:[anEvent locationInWindow] fromView:nil]]; + + if (marker) + { + [marker trackMouse:anEvent adding:NO]; + return; + } + + var client = [self clientView]; + + if ([client respondsToSelector:@selector(rulerView:handleMouseDown:)]) + [client rulerView:self handleMouseDown:anEvent]; +} + +/*! + Returns the menu for the marker under the pointer, if the client view supplies one. + @param anEvent the event that requested the menu + @return the menu, or \c nil +*/ +- (CPMenu)menuForEvent:(CPEvent)anEvent +{ + var marker = [self _markerAtPoint:[self convertPoint:[anEvent locationInWindow] fromView:nil]], + client = [self clientView]; + + if (marker && [client respondsToSelector:@selector(_rulerView:menuForMarker:)]) + return [client _rulerView:self menuForMarker:marker]; + + return [super menuForEvent:anEvent]; +} + +/* @ignore */ - (CPRulerMarker)_markerAtPoint:(CGPoint)aPoint { - // Search in reverse order so indent paddles (added last) are prioritized over overlapping 0-location tabs - for (var i = [_markers count] - 1; i >= 0; i--) + // The last marker is drawn on top, so it is hit first. + for (var i = _markers.length - 1; i >= 0; i--) { - var marker = [_markers objectAtIndex:i], - frame = [marker frame]; - - // Expanded hit-box by 3px on all sides for easy grabbing - var hitFrame = CGRectMake(frame.origin.x - 3.0, frame.origin.y - 2.0, frame.size.width + 6.0, frame.size.height + 4.0); - if (CGRectContainsPoint(hitFrame, aPoint)) + var marker = _markers[i]; + + if (CGRectContainsPoint(CGRectInset([marker imageRectInRuler], -2.0, -2.0), aPoint)) return marker; } + return nil; } -- (void)_positionMarker:(CPRulerMarker)aMarker +/* @ignore */ +- (void)_setTrackedMarker:(CPRulerMarker)aMarker { - if (!_scrollView) - return; - - var clipView = [_scrollView contentView], - scrollPoint = [clipView bounds].origin, - isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal), - rulerHeight = CGRectGetHeight([self bounds]), - rulerWidth = CGRectGetWidth([self bounds]), - markerLocation = [aMarker imageValue], - rep = [aMarker representedObject]; - - if (isHorizontal) - { - var x = markerLocation - scrollPoint.x - 6.0, - y = rulerHeight - 11.0, - w = 12.0, - h = 12.0; - - if (rep === @"CPFirstLineIndent") - { - y = 0.0; - h = Math.floor(rulerHeight / 2.0); - } - else if (rep === @"CPHeadIndent") - { - y = Math.floor(rulerHeight / 2.0); - h = rulerHeight - y - 1.0; - } - else - { - if (x < -6.0) x = -6.0; - else if (x + 12.0 > rulerWidth) x = rulerWidth - 12.0; - } - - [aMarker setFrame:CGRectMake(x, y, w, h)]; - } - else - { - var x = rulerWidth - 11.0, - y = markerLocation - scrollPoint.y - 6.0; - - 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)]; - } + _trackedMarker = aMarker; + [self setNeedsDisplay:YES]; } +// MARK: Client View Methods -// MARK: - Interaction Handlers - -- (void)mouseDown:(CPEvent)anEvent +/* @ignore */ +- (BOOL)_clientShouldAddMarker:(CPRulerMarker)aMarker { - 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); - - var clickedMarker = [self _markerAtPoint:localPoint]; - if (clickedMarker) - { - _draggingMarker = clickedMarker; - _dragStartPoint = localPoint; - _dragStartLocation = [_draggingMarker imageValue]; - } - else - { - // Allow client view to constrain initial placement - var client = [self clientView]; - if (client && [client respondsToSelector:@selector(rulerView:willAddMarker:atLocation:)]) - rulerLocation = [client rulerView:self willAddMarker:nil atLocation:rulerLocation]; - - var newMarker = [[CPRulerMarker alloc] initWithRulerView:self - markerLocation:rulerLocation - imageValue:rulerLocation - representedObject:nil]; - [self addMarker:newMarker]; - if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)]) - [client rulerView:self didAddMarker:newMarker]; - - _draggingMarker = newMarker; - _dragStartPoint = localPoint; - _dragStartLocation = rulerLocation; - } -} - -- (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 = Math.max(0.0, _dragStartLocation + delta); - - // Constrain marker location to last possible position var client = [self clientView]; - if (client && [client respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)]) - newLocation = [client rulerView:self willMoveMarker:_draggingMarker toLocation:newLocation]; - - [_draggingMarker setImageValue:newLocation]; - [self _positionMarker:_draggingMarker]; - - var rep = [_draggingMarker representedObject], - isIndent = (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent"); - - var draggedOff = !isIndent && (isHorizontal - ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15) - : (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15)); - - if (draggedOff) - { - [_draggingMarker setAlphaValue:0.4]; - [[_draggingMarker label] setTextColor:[CPColor grayColor]]; - } - else - { - [_draggingMarker setAlphaValue:1.0]; - if ([self isHUD]) - [[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.92 alpha:1.0]]; - else - [[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]]; - } - if (client && [client respondsToSelector:@selector(rulerView:didMoveMarker:)]) - [client rulerView:self didMoveMarker:_draggingMarker]; + return ![client respondsToSelector:@selector(rulerView:shouldAddMarker:)] || [client rulerView:self shouldAddMarker:aMarker]; } -- (void)mouseUp:(CPEvent)anEvent +/* @ignore */ +- (float)_clientWillAddMarker:(CPRulerMarker)aMarker atLocation:(float)aLocation { - if (!_draggingMarker) - return; + var client = [self clientView]; - var localPoint = [self convertPoint:[anEvent locationInWindow] fromView:nil], - isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal), - rep = [_draggingMarker representedObject], - isIndent = (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent"); - - var draggedOff = !isIndent && (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]; - } - else - { - [_draggingMarker setAlphaValue:1.0]; - if ([self isHUD]) - [[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.92 alpha:1.0]]; - else - [[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]]; - } - - _draggingMarker = nil; - [self updateRuler]; + return [client respondsToSelector:@selector(rulerView:willAddMarker:atLocation:)] ? [client rulerView:self willAddMarker:aMarker atLocation:aLocation] : aLocation; } - -// MARK: - Layout Builder - -- (void)updateRuler +/* @ignore */ +- (void)_clientDidAddMarker:(CPRulerMarker)aMarker { - [self setSubviews:@[]]; + var client = [self clientView]; - if (!_scrollView) - return; + if ([client respondsToSelector:@selector(rulerView:didAddMarker:)]) + [client rulerView:self didAddMarker:aMarker]; +} - var isHUD = [self isHUD]; +/* @ignore */ +- (BOOL)_clientShouldMoveMarker:(CPRulerMarker)aMarker +{ + var client = [self clientView]; - // Update ruler base background color - if (isHUD) - [self setBackgroundColor:[CPColor colorWithWhite:0.18 alpha:1.0]]; - else - [self setBackgroundColor:[CPColor colorWithWhite:0.96 alpha:1.0]]; + return ![client respondsToSelector:@selector(rulerView:shouldMoveMarker:)] || [client rulerView:self shouldMoveMarker:aMarker]; +} - var clipView = [_scrollView contentView], - scrollBounds = [clipView bounds], - scrollPoint = scrollBounds.origin, - visibleSize = scrollBounds.size, - isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal); +/* @ignore */ +- (float)_clientWillMoveMarker:(CPRulerMarker)aMarker toLocation:(float)aLocation +{ + var client = [self clientView]; - if (isHorizontal) - { - var start = Math.floor(scrollPoint.x / 10) * 10, - end = scrollPoint.x + visibleSize.width, - rulerHeight = CGRectGetHeight([self bounds]), - rulerWidth = CGRectGetWidth([self bounds]); + return [client respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)] ? [client rulerView:self willMoveMarker:aMarker toLocation:aLocation] : aLocation; +} - var bottomBorder = [[CPView alloc] initWithFrame:CGRectMake(0, rulerHeight - 1, rulerWidth, 1)]; - [bottomBorder setHitTests:NO]; - [bottomBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; - [self addSubview:bottomBorder]; +/* @ignore */ +- (void)_clientDidMoveMarker:(CPRulerMarker)aMarker +{ + var client = [self clientView]; - var firstLineMarker = nil, - headMarker = nil; - for (var i = 0; i < [_markers count]; i++) - { - var m = [_markers objectAtIndex:i]; - if ([m representedObject] === @"CPFirstLineIndent") - firstLineMarker = m; - else if ([m representedObject] === @"CPHeadIndent") - headMarker = m; - } + if ([client respondsToSelector:@selector(rulerView:didMoveMarker:)]) + [client rulerView:self didMoveMarker:aMarker]; +} - var halfHeight = Math.floor(rulerHeight / 2.0); +/* @ignore */ +- (BOOL)_clientShouldRemoveMarker:(CPRulerMarker)aMarker +{ + var client = [self clientView]; - if (firstLineMarker) - { - var firstLineX = [firstLineMarker imageValue] - scrollPoint.x; - if (firstLineX > 0) - { - var firstLineBg = [[CPView alloc] initWithFrame:CGRectMake(0, 0, firstLineX, halfHeight)]; - if (isHUD) - [firstLineBg setBackgroundColor:[CPColor colorWithWhite:0.24 alpha:1.0]]; - else - [firstLineBg setBackgroundColor:[CPColor colorWithWhite:0.93 alpha:1.0]]; - [self addSubview:firstLineBg]; - } - } + return ![client respondsToSelector:@selector(rulerView:shouldRemoveMarker:)] || [client rulerView:self shouldRemoveMarker:aMarker]; +} - // Draw Head Indent background - bottom half - if (headMarker) - { - var headX = [headMarker imageValue] - scrollPoint.x; - if (headX > 0) - { - var headBg = [[CPView alloc] initWithFrame:CGRectMake(0, halfHeight, headX, rulerHeight - halfHeight - 1.0)]; - if (isHUD) - [headBg setBackgroundColor:[CPColor colorWithWhite:0.28 alpha:1.0]]; - else - [headBg setBackgroundColor:[CPColor colorWithWhite:0.86 alpha:1.0]]; - [self addSubview:headBg]; - } - } +/* @ignore */ +- (void)_clientDidRemoveMarker:(CPRulerMarker)aMarker +{ + var client = [self clientView]; - // Render ruler tick lines and labels - 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; - - var tick = [[CPView alloc] initWithFrame:CGRectMake(screenX, tickY, 1.0, tickHeight)]; - if (isHUD) - [tick setBackgroundColor:isMajor ? [CPColor colorWithWhite:0.60 alpha:1.0] : [CPColor colorWithWhite:0.40 alpha:1.0]]; - else - [tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]]; - [self addSubview:tick]; - - if (isMajor) - { - var labelX = screenX - 20.0, - alignment = CPCenterTextAlignment; - - 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 setHitTests:NO]; - [label setStringValue:[CPString stringWithFormat:@"%d", val]]; - [label setFont:[CPFont systemFontOfSize:8.0]]; - if (isHUD) - [label setTextColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; - else - [label setTextColor:[CPColor colorWithWhite:0.40 alpha:1.0]]; - [label setAlignment:alignment]; - [self addSubview:label]; - } - } - } - else - { - // Vertical Ruler - var start = Math.floor(scrollPoint.y / 10) * 10, - end = scrollPoint.y + visibleSize.height, - rulerHeight = CGRectGetHeight([self bounds]), - rulerWidth = CGRectGetWidth([self bounds]); - - var rightBorder = [[CPView alloc] initWithFrame:CGRectMake(rulerWidth - 1, 0, 1, rulerHeight)]; - if (isHUD) - [rightBorder setBackgroundColor:[CPColor colorWithWhite:0.10 alpha:1.0]]; - else - [rightBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; - [self addSubview:rightBorder]; - - for (var val = start; val <= end; val += 10) - { - if (val < 0) continue; - - var screenY = val - scrollPoint.y, - isMajor = (val % 50 === 0), - tickWidth = isMajor ? 8.0 : 4.0, - tickX = rulerWidth - tickWidth - 1.0; - - var tick = [[CPView alloc] initWithFrame:CGRectMake(tickX, screenY, tickWidth, 1.0)]; - if (isHUD) - [tick setBackgroundColor:isMajor ? [CPColor colorWithWhite:0.60 alpha:1.0] : [CPColor colorWithWhite:0.40 alpha:1.0]]; - else - [tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]]; - [self addSubview:tick]; - - if (isMajor) - { - var labelY = screenY - 6.0; - - 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]]; - if (isHUD) - [label setTextColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; - else - [label setTextColor:[CPColor colorWithWhite:0.40 alpha:1.0]]; - [label setAlignment:CPRightTextAlignment]; - [self addSubview:label]; - } - } - } - - // Reposition active markers and keep their theme in sync - for (var i = 0; i < [_markers count]; i++) - { - var marker = [_markers objectAtIndex:i]; - if (isHUD) - [marker setThemeState:CPThemeStateHUD]; - else - [marker unsetThemeState:CPThemeStateHUD]; - - if ([marker superview] !== self) - [self addSubview:marker]; - [self _positionMarker:marker]; - } + if ([client respondsToSelector:@selector(rulerView:didRemoveMarker:)]) + [client rulerView:self didRemoveMarker:aMarker]; +} + +@end + + +var CPRulerViewOrientationKey = @"CPRulerViewOrientationKey", + CPRulerViewClientViewKey = @"CPRulerViewClientViewKey", + CPRulerViewAccessoryViewKey = @"CPRulerViewAccessoryViewKey", + CPRulerViewMarkersKey = @"CPRulerViewMarkersKey", + CPRulerViewRuleThicknessKey = @"CPRulerViewRuleThicknessKey", + CPRulerViewReservedThicknessForMarkersKey = @"CPRulerViewReservedThicknessForMarkersKey", + CPRulerViewReservedThicknessForAccessoryViewKey = @"CPRulerViewReservedThicknessForAccessoryViewKey", + CPRulerViewOriginOffsetKey = @"CPRulerViewOriginOffsetKey", + CPRulerViewMeasurementUnitsKey = @"CPRulerViewMeasurementUnitsKey"; + +@implementation CPRulerView (CPCoding) + +/*! + Initializes a ruler view from an archive. The scroll view that owns the ruler is not + archived; it sets itself as the ruler's scroll view when it is decoded. +*/ +- (id)initWithCoder:(CPCoder)aCoder +{ + self = [super initWithCoder:aCoder]; + + if (self) + { + _orientation = [aCoder decodeIntForKey:CPRulerViewOrientationKey]; + _clientView = [aCoder decodeObjectForKey:CPRulerViewClientViewKey]; + _accessoryView = [aCoder decodeObjectForKey:CPRulerViewAccessoryViewKey]; + _markers = [aCoder decodeObjectForKey:CPRulerViewMarkersKey] || []; + _rulerlineLocations = []; + + _ruleThickness = [aCoder decodeFloatForKey:CPRulerViewRuleThicknessKey]; + _reservedThicknessForMarkers = [aCoder decodeFloatForKey:CPRulerViewReservedThicknessForMarkersKey]; + _reservedThicknessForAccessoryView = [aCoder decodeFloatForKey:CPRulerViewReservedThicknessForAccessoryViewKey]; + _originOffset = [aCoder decodeFloatForKey:CPRulerViewOriginOffsetKey]; + _measurementUnits = [aCoder decodeObjectForKey:CPRulerViewMeasurementUnitsKey] || [[self class] _defaultMeasurementUnits]; + + [self invalidateHashMarks]; + } + + return self; +} + +/*! + Archives the ruler view. The client view is archived conditionally. +*/ +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [super encodeWithCoder:aCoder]; + + [aCoder encodeInt:_orientation forKey:CPRulerViewOrientationKey]; + [aCoder encodeConditionalObject:_clientView forKey:CPRulerViewClientViewKey]; + [aCoder encodeObject:_accessoryView forKey:CPRulerViewAccessoryViewKey]; + [aCoder encodeObject:_markers forKey:CPRulerViewMarkersKey]; + [aCoder encodeFloat:_ruleThickness forKey:CPRulerViewRuleThicknessKey]; + [aCoder encodeFloat:_reservedThicknessForMarkers forKey:CPRulerViewReservedThicknessForMarkersKey]; + [aCoder encodeFloat:_reservedThicknessForAccessoryView forKey:CPRulerViewReservedThicknessForAccessoryViewKey]; + [aCoder encodeFloat:_originOffset forKey:CPRulerViewOriginOffsetKey]; + [aCoder encodeObject:_measurementUnits forKey:CPRulerViewMeasurementUnitsKey]; } @end