Files
cappuccino/AppKit/CPTextView/CPRulerView.j
T
David Richardson 8e4742a250 AppKit: Carry Interface Builder's Uses Ruler setting into cibs
nib2cib ignored the text view's Uses Ruler setting, and CPTextView, CPRulerView and CPRulerMarker archived none of their ruler state, so a ruler could not be configured in Interface Builder or survive archiving.

* CPScrollView.j: Set itself as the scroll view of decoded rulers.

* CPTextView/CPRulerMarker.j: Archive the marker's ruler, location, image and image origin, movability, removability and represented object.

* CPTextView/CPRulerView.j: Archive the ruler's orientation, client view, accessory view, markers, thicknesses, origin offset and measurement units.

* CPTextView/CPTextView.j: Archive usesRuler.

* Tools/nib2cib/NSTextView.j, Tools/nib2cib/NSTextViewSharedData.j: Read the Uses Ruler flag.

* Tests/AppKit/CPRulerViewTest.j: Add tests for ruler layout, measurement units, archiving and the Uses Ruler setting.

* Tests/Manual/TextEditor/Resources/MainMenu.xib, Resources/MainMenu.cib: Add the standard Format menu, whose Text submenu shows and hides the ruler.

* Tests/Manual/TextEditor/Resources/DocumentWindow.xib, Resources/DocumentWindow.cib: Rebuild the cib with the ruler setting, and adjust the window's initial position.
2026-10-23 21:10:17 -06:00

978 lines
30 KiB
Plaintext

/*
* 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
*/
@import <Foundation/CPDictionary.j>
@import <Foundation/CPNotificationCenter.j>
@import <Foundation/CPObject.j>
@import "CGContext.j"
@import "CGGeometry.j"
@import "CPColor.j"
@import "CPFont.j"
@import "CPGraphicsContext.j"
@import "CPImage.j"
@import "CPRulerMarker.j"
@import "CPView.j"
@class CPMenu
@class CPScrollView
@typedef CPRulerOrientation
CPHorizontalRuler = 0;
CPVerticalRuler = 1;
CPRulerOrientationHorizontal = CPHorizontalRuler;
CPRulerOrientationVertical = CPVerticalRuler;
/*!
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 = @{};
/*
The smallest distances, in points, kept between two labels and between two hash marks.
*/
var CPRulerViewMinimumLabelSpacing = 40.0,
CPRulerViewMinimumHashMarkSpacing = 5.0;
/*!
@ingroup appkit
@class CPRulerView
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.
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.
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:
<pre>
- (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;
</pre>
*/
@implementation CPRulerView : CPView
{
CPScrollView _scrollView;
CPRulerOrientation _orientation;
CPView _clientView;
CPView _accessoryView;
CPArray _markers;
CPRulerMarker _trackedMarker;
CPArray _rulerlineLocations;
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
{
self = [super initWithFrame:CGRectMakeZero()];
if (self)
{
_scrollView = aScrollView;
_orientation = anOrientation;
_markers = [];
_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;
}
// MARK: Ruler Layout
/*!
Returns the scroll view that owns the ruler.
*/
- (CPScrollView)scrollView
{
return _scrollView;
}
/*!
Sets the scroll view that owns the ruler. The scroll view is not retained.
@param aScrollView the owning scroll view
*/
- (void)setScrollView:(CPScrollView)aScrollView
{
_scrollView = aScrollView;
}
/*!
Returns the ruler's orientation: \c CPHorizontalRuler or \c CPVerticalRuler.
*/
- (CPRulerOrientation)orientation
{
return _orientation;
}
/*!
Sets the ruler's orientation.
@param anOrientation \c CPHorizontalRuler or \c CPVerticalRuler
*/
- (void)setOrientation:(CPRulerOrientation)anOrientation
{
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
{
if (_clientView === aView)
return;
var currentClient = [self clientView];
if ([currentClient respondsToSelector:@selector(rulerView:willSetClientView:)])
[currentClient rulerView:self willSetClientView:aView];
_clientView = aView;
[self setMarkers:nil];
}
// MARK: Setting an Accessory View
/*!
Returns the ruler's accessory view.
*/
- (CPView)accessoryView
{
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
{
_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: 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, [CPColor colorWithWhite:0.96 alpha:1.0]);
CGContextFillRect(context, bounds);
// The edge next to the scroll view's content.
CGContextSetFillColor(context, [CPColor colorWithWhite:0.75 alpha:1.0]);
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);
CGContextSetFillColor(context, [CPColor colorWithWhite:0.55 alpha:1.0]);
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];
// 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);
}
}
/*!
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, [CPColor colorWithWhite:0.3 alpha:1.0]);
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
{
// The last marker is drawn on top, so it is hit first.
for (var i = _markers.length - 1; i >= 0; i--)
{
var marker = _markers[i];
if (CGRectContainsPoint(CGRectInset([marker imageRectInRuler], -2.0, -2.0), aPoint))
return marker;
}
return nil;
}
/* @ignore */
- (void)_setTrackedMarker:(CPRulerMarker)aMarker
{
_trackedMarker = aMarker;
[self setNeedsDisplay:YES];
}
// MARK: Client View Methods
/* @ignore */
- (BOOL)_clientShouldAddMarker:(CPRulerMarker)aMarker
{
var client = [self clientView];
return ![client respondsToSelector:@selector(rulerView:shouldAddMarker:)] || [client rulerView:self shouldAddMarker:aMarker];
}
/* @ignore */
- (float)_clientWillAddMarker:(CPRulerMarker)aMarker atLocation:(float)aLocation
{
var client = [self clientView];
return [client respondsToSelector:@selector(rulerView:willAddMarker:atLocation:)] ? [client rulerView:self willAddMarker:aMarker atLocation:aLocation] : aLocation;
}
/* @ignore */
- (void)_clientDidAddMarker:(CPRulerMarker)aMarker
{
var client = [self clientView];
if ([client respondsToSelector:@selector(rulerView:didAddMarker:)])
[client rulerView:self didAddMarker:aMarker];
}
/* @ignore */
- (BOOL)_clientShouldMoveMarker:(CPRulerMarker)aMarker
{
var client = [self clientView];
return ![client respondsToSelector:@selector(rulerView:shouldMoveMarker:)] || [client rulerView:self shouldMoveMarker:aMarker];
}
/* @ignore */
- (float)_clientWillMoveMarker:(CPRulerMarker)aMarker toLocation:(float)aLocation
{
var client = [self clientView];
return [client respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)] ? [client rulerView:self willMoveMarker:aMarker toLocation:aLocation] : aLocation;
}
/* @ignore */
- (void)_clientDidMoveMarker:(CPRulerMarker)aMarker
{
var client = [self clientView];
if ([client respondsToSelector:@selector(rulerView:didMoveMarker:)])
[client rulerView:self didMoveMarker:aMarker];
}
/* @ignore */
- (BOOL)_clientShouldRemoveMarker:(CPRulerMarker)aMarker
{
var client = [self clientView];
return ![client respondsToSelector:@selector(rulerView:shouldRemoveMarker:)] || [client rulerView:self shouldRemoveMarker:aMarker];
}
/* @ignore */
- (void)_clientDidRemoveMarker:(CPRulerMarker)aMarker
{
var client = [self clientView];
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