Files
cappuccino/AppKit/CPTextView/CPRulerView.j
T
2026-05-28 17:51:10 +02:00

1088 lines
33 KiB
Plaintext

/*
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 <AppKit/CPView.j>
@import <AppKit/CPScrollView.j>
@import <AppKit/CPBezierPath.j>
@import <AppKit/CPColor.j>
@import <AppKit/CPStringDrawing.j>
@import <AppKit/CPFont.j>
@import <AppKit/CPImage.j>
@import <Foundation/CPAttributedString.j>
@class CPRulerMarker
@global CPHorizontalRuler
@global CPVerticalRuler
// Constants for layout
var DEFAULT_RULE_THICKNESS = 16.0,
DEFAULT_MARKER_THICKNESS = 15.0,
HASH_MARK_THICKNESS_FACTOR = 0.6,
HASH_MARK_WIDTH = 1.0,
LABEL_TEXT_PADDING = 2.0;
var _measurementUnits = nil;
// -----------------------------------------------------------------------------
// Helper Class: _CPMeasurementUnit
// -----------------------------------------------------------------------------
@implementation _CPMeasurementUnit : CPObject
{
CPString _name @accessors(property = name);
CPString _abbreviation @accessors(property = abbreviation);
float _pointsPerUnit @accessors(property = pointsPerUnit);
CPArray _stepUpCycle @accessors(property = stepUpCycle);
CPArray _stepDownCycle @accessors(property = stepDownCycle);
}
+ (void)initialize
{
if (self !== [_CPMeasurementUnit class])
return;
_measurementUnits = [];
[self registerUnit:[_CPMeasurementUnit inchesMeasurementUnit]];
[self registerUnit:[_CPMeasurementUnit centimetersMeasurementUnit]];
[self registerUnit:[_CPMeasurementUnit pointsMeasurementUnit]];
[self registerUnit:[_CPMeasurementUnit picasMeasurementUnit]];
}
+ (_CPMeasurementUnit)measurementUnitWithName:(CPString)name abbreviation:(CPString)abbreviation pointsPerUnit:(float)points stepUpCycle:(CPArray)upCycle stepDownCycle:(CPArray)downCycle
{
return [[self alloc] initWithName:name abbreviation:abbreviation pointsPerUnit:points stepUpCycle:upCycle stepDownCycle:downCycle];
}
- (id)initWithName:(CPString)name abbreviation:(CPString)abbreviation pointsPerUnit:(float)points stepUpCycle:(CPArray)upCycle stepDownCycle:(CPArray)downCycle
{
self = [super init];
if (self)
{
_name = name;
_abbreviation = abbreviation;
_pointsPerUnit = points;
_stepUpCycle = upCycle;
_stepDownCycle = downCycle;
}
return self;
}
+ (_CPMeasurementUnit)inchesMeasurementUnit
{
return [self measurementUnitWithName:@"Inches"
abbreviation:@"in"
pointsPerUnit:72.0
stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ]
stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.25], [CPNumber numberWithFloat:0.125] ]];
}
+ (_CPMeasurementUnit)centimetersMeasurementUnit
{
return [self measurementUnitWithName:@"Centimeters"
abbreviation:@"cm"
pointsPerUnit:28.3465
stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ]
stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.1] ]];
}
+ (_CPMeasurementUnit)pointsMeasurementUnit
{
return [self measurementUnitWithName:@"Points"
abbreviation:@"pt"
pointsPerUnit:1.0
stepUpCycle:[ [CPNumber numberWithFloat:10.0], [CPNumber numberWithFloat:50.0], [CPNumber numberWithFloat:100.0] ]
stepDownCycle:[ [CPNumber numberWithFloat:0.5] ]]; // Points rarely sub-divide
}
+ (_CPMeasurementUnit)picasMeasurementUnit
{
return [self measurementUnitWithName:@"Picas"
abbreviation:@"pc"
pointsPerUnit:12.0
stepUpCycle:[ [CPNumber numberWithFloat:2.0], [CPNumber numberWithFloat:5.0], [CPNumber numberWithFloat:10.0] ]
stepDownCycle:[ [CPNumber numberWithFloat:0.5], [CPNumber numberWithFloat:0.0833] ]]; // 6p and 1pt
}
+ (CPArray)allMeasurementUnits
{
return _measurementUnits;
}
+ (_CPMeasurementUnit)measurementUnitNamed:(CPString)name
{
var i = 0, count = [_measurementUnits count];
for (; i < count; ++i)
if ([[[_measurementUnits objectAtIndex:i] name] isEqualToString:name])
return [_measurementUnits objectAtIndex:i];
return nil;
}
+ (void)registerUnit:(_CPMeasurementUnit)unit
{
if (!_measurementUnits) _measurementUnits = [];
[_measurementUnits addObject:unit];
}
@end
// -----------------------------------------------------------------------------
// Class: CPRulerView
// -----------------------------------------------------------------------------
@implementation CPRulerView : CPView
{
CPScrollView _scrollView;
CPView _clientView;
CPView _accessoryView;
CPArray _markers;
_CPMeasurementUnit _measurementUnit;
CPMutableArray _rulerlineLocations;
float _originOffset;
float _ruleThickness;
float _thicknessForMarkers;
float _thicknessForAccessoryView;
CPRulerOrientation _orientation;
// Cache attributes
CPDictionary _labelAttributes;
}
// MARK: - Class Methods
+ (void)registerUnitWithName:(CPString)name abbreviation:(CPString)abbreviation unitToPointsConversionFactor:(float)conversionFactor stepUpCycle:(CPArray)stepUpCycle stepDownCycle:(CPArray)stepDownCycle
{
[_CPMeasurementUnit registerUnit:[_CPMeasurementUnit measurementUnitWithName:name abbreviation:abbreviation pointsPerUnit:conversionFactor stepUpCycle:stepUpCycle stepDownCycle:stepDownCycle]];
}
// MARK: - Initialization
- (id)initWithFrame:(CPRect)frame
{
return [self initWithScrollView:nil orientation:CPHorizontalRuler];
}
- (id)initWithScrollView:(CPScrollView)scrollView orientation:(CPRulerOrientation)orientation
{
var frame = CPMakeRect(0, 0, 1, 1);
// Determine initial frame based on standard thickness
if (orientation == CPHorizontalRuler)
frame.size.height = DEFAULT_RULE_THICKNESS;
else
frame.size.width = DEFAULT_RULE_THICKNESS;
self = [super initWithFrame:frame];
if (self)
{
_scrollView = scrollView;
_orientation = orientation;
_measurementUnit = [_CPMeasurementUnit measurementUnitNamed:@"Inches"];
_ruleThickness = DEFAULT_RULE_THICKNESS;
_thicknessForMarkers = 0.0; // Grows as needed
_thicknessForAccessoryView = 0.0;
_originOffset = 0.0;
_markers = [];
_rulerlineLocations = [];
var style = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
[style setLineBreakMode:CPLineBreakByClipping];
[style setAlignment:CPLeftTextAlignment];
_labelAttributes = @{
CPFontAttributeName: [CPFont systemFontOfSize:9.0],
CPParagraphStyleAttributeName: style,
CPForegroundColorAttributeName: [CPColor blackColor]
};
}
return self;
}
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super initWithCoder:aCoder];
if (self)
{
_scrollView = [aCoder decodeObjectForKey:@"CPScrollView"];
_orientation = [aCoder decodeIntForKey:@"CPOrientation"];
_markers = [aCoder decodeObjectForKey:@"CPMarkers"];
if (!_markers) _markers = [];
// Defaults
_ruleThickness = DEFAULT_RULE_THICKNESS;
_measurementUnit = [_CPMeasurementUnit measurementUnitNamed:@"Inches"];
_rulerlineLocations = [];
}
return self;
}
// MARK: - Layout & Metrics
- (float)reservedThicknessForMarkers
{
if ([_markers count] > 0 && _thicknessForMarkers < DEFAULT_MARKER_THICKNESS)
return DEFAULT_MARKER_THICKNESS;
return _thicknessForMarkers;
}
- (float)reservedThicknessForAccessoryView
{
return _thicknessForAccessoryView;
}
- (float)ruleThickness
{
return _ruleThickness;
}
- (float)requiredThickness
{
var result = [self ruleThickness];
if ([_markers count] > 0)
result += [self reservedThicknessForMarkers];
if (_accessoryView)
result += [self reservedThicknessForAccessoryView];
return result;
}
- (float)baselineLocation
{
// The baseline is the line separating the ruler from the content.
// In a horizontal ruler, it's the bottom edge (height).
// Usually, the hash marks grow upwards from this baseline.
return [self bounds].size.height;
}
// MARK: - Accessors
- (CPScrollView)scrollView
{
return _scrollView;
}
- (void)setScrollView:(CPScrollView)scrollView
{
_scrollView = scrollView;
[self setNeedsDisplay:YES];
}
- (CPRulerOrientation)orientation
{
return _orientation;
}
- (void)setOrientation:(CPRulerOrientation)orientation
{
_orientation = orientation;
[self setNeedsDisplay:YES];
}
- (CPView)clientView
{
return _clientView;
}
- (void)setClientView:(CPView)view
{
if (_clientView === view) return;
if ([_clientView respondsToSelector:@selector(rulerView:willSetClientView:)])
[_clientView rulerView:self willSetClientView:view];
// Standard behavior: clear markers when client changes unless preserved
[_markers removeAllObjects];
_clientView = view;
[self invalidateHashMarks];
[[self enclosingScrollView] tile];
}
- (CPView)accessoryView
{
return _accessoryView;
}
- (void)setAccessoryView:(CPView)view
{
if (_accessoryView === view) return;
[_accessoryView removeFromSuperview];
_accessoryView = view;
if (_accessoryView)
{
[self addSubview:_accessoryView];
// Usually you'd set the frame here based on thickness
}
[[self enclosingScrollView] tile];
}
- (CPArray)markers
{
return _markers;
}
- (void)setMarkers:(CPArray)markers
{
if (_markers === markers) return;
_markers = [markers mutableCopy];
[[self enclosingScrollView] tile];
[self setNeedsDisplay:YES];
}
- (void)addMarker:(CPRulerMarker)marker
{
[_markers addObject:marker];
[marker setRulerView:self]; // Ensure marker knows its ruler
[[self enclosingScrollView] tile];
[self setNeedsDisplay:YES];
}
- (void)removeMarker:(CPRulerMarker)marker
{
[_markers removeObject:marker];
[[self enclosingScrollView] tile];
[self setNeedsDisplay:YES];
}
- (void)setMeasurementUnits:(CPString)unitName
{
var unit = [_CPMeasurementUnit measurementUnitNamed:unitName];
if (unit)
{
_measurementUnit = unit;
[self setNeedsDisplay:YES];
}
}
- (CPString)measurementUnits
{
return [_measurementUnit name];
}
- (void)setOriginOffset:(float)value
{
_originOffset = value;
[self setNeedsDisplay:YES];
}
- (float)originOffset
{
return _originOffset;
}
- (void)setRuleThickness:(float)value
{
_ruleThickness = value;
[[self enclosingScrollView] tile];
}
- (void)setReservedThicknessForMarkers:(float)value
{
_thicknessForMarkers = value;
[[self enclosingScrollView] tile];
}
- (void)setReservedThicknessForAccessoryView:(float)value
{
_thicknessForAccessoryView = value;
[[self enclosingScrollView] tile];
}
// MARK: - Event Handling
- (BOOL)trackMarker:(CPRulerMarker)marker withMouseEvent:(CPEvent)event
{
// Convert event location to ruler coordinates
var point = [self convertPoint:[event locationInWindow] fromView:nil];
// Basic hit testing logic
if (CPPointInRect(point, [self bounds]))
{
// Marker implements trackMouse:adding:
if ([marker respondsToSelector:@selector(trackMouse:adding:)])
{
[marker trackMouse:event adding:YES];
}
[self setNeedsDisplay:YES];
return YES;
}
return NO;
}
- (void)mouseDown:(CPEvent)event
{
var point = [self convertPoint:[event locationInWindow] fromView:nil],
i = 0, count = [_markers count];
// 1. Check if an existing marker was clicked
for (; i < count; ++i)
{
var marker = [_markers objectAtIndex:i];
if (CPPointInRect(point, [marker imageRectInRuler]))
{
[marker trackMouse:event adding:NO];
[self setNeedsDisplay:YES];
return;
}
}
// 2. Delegate to Client View (e.g. to create a new guide/marker)
if ([_clientView respondsToSelector:@selector(rulerView:handleMouseDown:)])
[_clientView rulerView:self handleMouseDown:event];
}
// MARK: - Drawing Support
- (void)moveRulerlineFromLocation:(float)fromLocation toLocation:(float)toLocation
{
var oldLoc = [CPNumber numberWithFloat:fromLocation],
newLoc = [CPNumber numberWithFloat:toLocation];
[_rulerlineLocations removeObject:oldLoc];
// Only add if it's not effectively "off" (using -1 as a convention for hiding)
if (toLocation >= 0)
[_rulerlineLocations addObject:newLoc];
[self setNeedsDisplay:YES];
}
- (void)invalidateHashMarks
{
[self setNeedsDisplay:YES];
}
// MARK: - Drawing Implementation
- (BOOL)isFlipped
{
if (_orientation == CPHorizontalRuler)
return YES; // Horizontal rulers usually draw top-down or match standard flipped views
// For vertical rulers, we usually want to match the document view's flip state
// so numbers increase going down if the document does.
if (_clientView)
return [_clientView isFlipped];
return YES;
}
- (float)_drawingScale
{
// Zoom support
var scale = 1.0,
docView = [_scrollView documentView];
if (docView && [docView superview])
{
// Calculate scale based on bounds vs frame
var bounds = [docView bounds],
frame = [docView frame];
if (_orientation == CPHorizontalRuler)
scale = frame.size.width / bounds.size.width;
else
scale = frame.size.height / bounds.size.height;
}
return scale;
}
- (float)_drawingOrigin
{
// Calculate the point in the ruler that corresponds to "0" in the client view
var origin = 0.0,
trackedView = _clientView;
if (!trackedView)
trackedView = [_scrollView documentView];
if (!trackedView) return 0.0;
// Convert (0,0) of the tracked view to the ruler's coordinate space
var viewZero = [self convertPoint:CGPointMake(0,0) fromView:trackedView];
if (_orientation == CPHorizontalRuler)
origin = viewZero.x;
else
origin = viewZero.y;
// Apply user-defined offset (e.g. if the user dragged the zero-point)
// Note: originOffset is in client coordinates, so we scale it.
origin += (_originOffset * [self _drawingScale]);
return origin;
}
- (void)drawHashMarksAndLabelsInRect:(CPRect)dirtyRect
{
var bounds = [self bounds],
scale = [self _drawingScale],
zeroLocation = [self _drawingOrigin], // x position (horiz) or y position (vert) where 0 is
pointsPerUnit = [_measurementUnit pointsPerUnit] * scale;
// Avoid divide by zero
if (pointsPerUnit <= 0) return;
var isHorizontal = (_orientation == CPHorizontalRuler),
ruleSize = isHorizontal ? bounds.size.height : bounds.size.width;
// Adjust for markers/accessory thickness area
var reserved = 0.0;
if ([_markers count] > 0) reserved += [self reservedThicknessForMarkers];
if (_accessoryView) reserved += [self reservedThicknessForAccessoryView];
// The actual area for hashes
var hashAreaSize = ruleSize - reserved,
hashBaseline = reserved; // Drawing starts after reserved area
// Calculate range of units to draw based on dirtyRect
var startPos = isHorizontal ? dirtyRect.origin.x : dirtyRect.origin.y,
endPos = isHorizontal ? CPReectGetMaxX(dirtyRect) : CPReectGetMaxY(dirtyRect);
// Convert view coordinates to Unit coordinates
// pos = zeroLocation + (unit * pointsPerUnit)
// unit = (pos - zeroLocation) / pointsPerUnit
var startUnit = Math.floor((startPos - zeroLocation) / pointsPerUnit),
endUnit = Math.ceil((endPos - zeroLocation) / pointsPerUnit);
var stepDowns = [_measurementUnit stepDownCycle],
numSteps = [stepDowns count];
[[CPColor grayColor] setStroke];
// Iterate through units
for (var u = startUnit; u <= endUnit; u++)
{
var unitPos = zeroLocation + (u * pointsPerUnit);
// 1. Draw Major Mark
var majorHeight = hashAreaSize * HASH_MARK_THICKNESS_FACTOR;
[self _drawHashAt:unitPos length:majorHeight offset:hashBaseline horizontal:isHorizontal];
// 2. Draw Label (only for major units)
var labelStr = [CPString stringWithFormat:@"%d", u];
// Simple label positioning
var labelPoint;
if (isHorizontal)
labelPoint = CPMakePoint(unitPos + LABEL_TEXT_PADDING, hashBaseline);
else
labelPoint = CPMakePoint(hashBaseline + LABEL_TEXT_PADDING, unitPos);
[labelStr drawAtPoint:labelPoint withAttributes:_labelAttributes];
// 3. Draw Subdivisions
// We handle a simple single-level subdivision for performance,
// or iterate the cycle. Let's do a simple iterative cycle.
// E.g. stepDownCycle: [0.5, 0.25, 0.125] implies 1/2, then 1/4, etc.
// But the CPMeasurementUnit spec in the prompt suggests relative steps.
// Let's assume the array contains fractions of the unit: e.g. [0.5, 0.1]
for (var s = 0; s < numSteps; s++)
{
var stepFraction = [[stepDowns objectAtIndex:s] floatValue];
if (stepFraction <= 0) continue;
// Determine visual spacing. If marks are too close, stop recursing.
var pixelsPerStep = pointsPerUnit * stepFraction;
if (pixelsPerStep < 4.0) break;
// Calculate height: gradually smaller
var subHeight = majorHeight * (0.7 / (s + 1));
// How many marks fit in one unit? 1 / fraction.
// e.g. 0.5 -> 2 marks (at 0.0 and 0.5). 0.0 is major, so we draw at 0.5.
var subCount = Math.round(1.0 / stepFraction);
for (var k = 1; k < subCount; k++)
{
// We only draw if this position wasn't covered by a higher-level step.
// Simplified: Just draw everything, simpler logic for UI.
// Optimization: Skip if integer check passes? No, keep it simple.
var subUnitOffset = k * stepFraction;
// Only draw if this isn't a whole integer (covered by major)
if (subUnitOffset % 1.0 === 0) continue;
var subPos = unitPos + (subUnitOffset * pointsPerUnit);
// Bounds check optimization
if (subPos < startPos || subPos > endPos) continue;
[self _drawHashAt:subPos length:subHeight offset:hashBaseline horizontal:isHorizontal];
}
}
}
}
- (void)_drawHashAt:(float)pos length:(float)length offset:(float)offset horizontal:(BOOL)isHorizontal
{
var path = [CPBezierPath bezierPath];
[path setLineWidth:1.0];
if (isHorizontal)
{
// Draw vertical line at 'pos'
[path moveToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height - length)]; // Draw from bottom up
[path lineToPoint:CPMakePoint(pos + 0.5, [self bounds].size.height)];
// Or if flipped (top-down), draw from offset down
// Since we force isFlipped=YES for horizontal, y=0 is top.
// Typically rulers align marks to the edge touching the content.
[path moveToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height - length)];
[path lineToPoint:CPMakePoint(pos + 0.5, offset + [self bounds].size.height)];
}
else
{
// Draw horizontal line at 'pos'
[path moveToPoint:CPMakePoint(offset + [self bounds].size.width - length, pos + 0.5)];
[path lineToPoint:CPMakePoint(offset + [self bounds].size.width, pos + 0.5)];
}
[path stroke];
}
- (void)drawMarkersInRect:(CPRect)dirtyRect
{
var count = [_markers count];
for (var i = 0; i < count; i++)
{
var m = [_markers objectAtIndex:i];
if (CPRectIntersectsRect([m imageRectInRuler], dirtyRect))
[m drawRect:dirtyRect];
}
}
- (void)drawRulerlineLocationsInRect:(CPRect)rect
{
var count = [_rulerlineLocations count];
if (count === 0) return;
[[CPColor controlShadowColor] setStroke];
var bounds = [self bounds];
for (var i = 0; i < count; ++i)
{
var loc = [[_rulerlineLocations objectAtIndex:i] floatValue];
var path = [CPBezierPath bezierPath];
if (_orientation == CPHorizontalRuler)
{
[path moveToPoint:CPMakePoint(loc + 0.5, 0)];
[path lineToPoint:CPMakePoint(loc + 0.5, bounds.size.height)];
}
else
{
[path moveToPoint:CPMakePoint(0, loc + 0.5)];
[path lineToPoint:CPMakePoint(bounds.size.width, loc + 0.5)];
}
[path stroke];
}
}
- (void)drawRect:(CPRect)dirtyRect
{
var context = [[CPGraphicsContext currentContext] graphicsPort];
// 1. Draw Background
[[CPColor colorWithCalibratedWhite:0.96 alpha:1.0] setFill];
CGContextFillRect(context, dirtyRect);
// 2. Draw Bottom/Right Border (Baseline)
[[CPColor darkGrayColor] setStroke];
var borderPath = [CPBezierPath bezierPath];
var bounds = [self bounds];
if (_orientation == CPHorizontalRuler)
{
[borderPath moveToPoint:CPMakePoint(0, bounds.size.height - 0.5)];
[borderPath lineToPoint:CPMakePoint(bounds.size.width, bounds.size.height - 0.5)];
}
else
{
[borderPath moveToPoint:CPMakePoint(bounds.size.width - 0.5, 0)];
[borderPath lineToPoint:CPMakePoint(bounds.size.width - 0.5, bounds.size.height)];
}
[borderPath stroke];
// 3. Draw Contents
[self drawHashMarksAndLabelsInRect:dirtyRect];
if ([_markers count] > 0)
[self drawMarkersInRect:dirtyRect];
if ([_rulerlineLocations count] > 0)
[self drawRulerlineLocationsInRect:dirtyRect];
}
@end
@implementation CPRulerMarker : CPObject
{
CPRulerView _ruler;
float _markerLocation;
CPImage _image;
CPPoint _imageOrigin;
BOOL _movable @accessors(property=movable);
BOOL _removable @accessors(property=removable);
id _representedObject @accessors(property=representedObject);
// Internal state
BOOL _dragging @accessors(getter=isDragging);
}
// MARK: - Initialization
- (id)initWithRulerView:(CPRulerView)aRuler markerLocation:(float)location image:(CPImage)anImage imageOrigin:(CPPoint)anImageOrigin
{
self = [super init];
if (self)
{
_ruler = aRuler;
_markerLocation = location;
_image = anImage;
_imageOrigin = anImageOrigin;
_movable = NO;
_removable = NO;
_dragging = NO;
}
return self;
}
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super init];
if (self)
{
// Ruler is usually assigned after decoding by the view hierarchy
_markerLocation = [aCoder decodeFloatForKey:@"CPMarkerLocation"];
_image = [aCoder decodeObjectForKey:@"CPImage"];
_imageOrigin = [aCoder decodePointForKey:@"CPImageOrigin"];
_movable = [aCoder decodeBoolForKey:@"CPMovable"];
_removable = [aCoder decodeBoolForKey:@"CPRemovable"];
_representedObject = [aCoder decodeObjectForKey:@"CPRepresentedObject"];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeFloat:_markerLocation forKey:@"CPMarkerLocation"];
[aCoder encodeObject:_image forKey:@"CPImage"];
[aCoder encodePoint:_imageOrigin forKey:@"CPImageOrigin"];
[aCoder encodeBool:_movable forKey:@"CPMovable"];
[aCoder encodeBool:_removable forKey:@"CPRemovable"];
[aCoder encodeObject:_representedObject forKey:@"CPRepresentedObject"];
}
- (id)copy
{
var copy = [[CPRulerMarker alloc] initWithRulerView:_ruler
markerLocation:_markerLocation
image:_image
imageOrigin:_imageOrigin];
[copy setMovable:_movable];
[copy setRemovable:_removable];
[copy setRepresentedObject:_representedObject];
return copy;
}
// MARK: - Accessors
- (CPRulerView)ruler
{
return _ruler;
}
// Used when adding a marker to a ruler
- (void)setRulerView:(CPRulerView)aRuler
{
_ruler = aRuler;
}
- (CPImage)image
{
return _image;
}
- (void)setImage:(CPImage)anImage
{
_image = anImage;
[_ruler setNeedsDisplay:YES];
}
- (CPPoint)imageOrigin
{
return _imageOrigin;
}
- (void)setImageOrigin:(CPPoint)aPoint
{
_imageOrigin = aPoint;
[_ruler setNeedsDisplay:YES];
}
- (float)markerLocation
{
return _markerLocation;
}
- (void)setMarkerLocation:(float)location
{
if (_markerLocation === location) return;
_markerLocation = location;
// Invalidate the ruler to redraw at new position
[_ruler setNeedsDisplay:YES];
}
// MARK: - Geometry
- (float)thicknessRequiredInRuler
{
if (!_image) return 0.0;
// If horizontal ruler, thickness is image height.
// If vertical ruler, thickness is image width.
var size = [_image size];
if ([_ruler orientation] === CPHorizontalRuler)
return size.height;
else
return size.width;
}
- (CPRect)imageRectInRuler
{
if (!_ruler || !_image) return CPMakeRect(0,0,0,0);
var clientView = [_ruler clientView];
// If no client view, fallback to document view or 0
if (!clientView) clientView = [[_ruler scrollView] documentView];
var rulerBounds = [_ruler bounds],
imageSize = [_image size],
rect = CPMakeRect(0, 0, imageSize.width, imageSize.height);
// 1. Convert Marker Location (Client Coords) to Ruler Coords
var locationInRuler = 0.0;
if (clientView)
{
// We create a point in the client view at the marker location
var ptInClient = CPMakePoint(0, 0);
if ([_ruler orientation] === CPHorizontalRuler)
ptInClient.x = _markerLocation;
else
ptInClient.y = _markerLocation;
// Convert that point to the ruler's coordinate system
var ptInRuler = [_ruler convertPoint:ptInClient fromView:clientView];
locationInRuler = ([_ruler orientation] === CPHorizontalRuler) ? ptInRuler.x : ptInRuler.y;
}
// Apply user offset from CPRulerView (originOffset)
// Note: The conversion above usually handles view transforms, but if CPRulerView
// manually applies an extra offset property (like in the previous implementation),
// we should respect it implicitly via the conversion or explicitly here.
// Assuming standard view conversion handles the scroll/bounds.
// 2. Position the rect based on Orientation and Image Origin
// imageOrigin is the point *inside* the image that aligns with 'locationInRuler'
if ([_ruler orientation] === CPHorizontalRuler)
{
// X: Aligned by location minus the hotspot X
rect.origin.x = locationInRuler - _imageOrigin.x;
// Y: This depends on the ruler's baseline.
// Typically horizontal markers sit on the bottom edge (baseline).
// If the image origin Y is the "tip", we position relative to that.
// Usually, imageOrigin.y is 0 (bottom) or Height (top) depending on coordinate flip.
// Assuming the ruler draws labels near the baseline and the marker sits on it:
// Let's assume the baseline is the bottom of the ruler rect.
var baseline = [_ruler baselineLocation];
rect.origin.y = baseline - _imageOrigin.y;
// If flipped, adjustments might be needed, but usually image drawing handles internal flip.
}
else
{
// Vertical Ruler
// Y: Aligned by location minus hotspot Y
rect.origin.y = locationInRuler - _imageOrigin.y;
// X: Align to right edge (baseline)
var baseline = [_ruler baselineLocation]; // Width for vertical ruler
rect.origin.x = baseline - _imageOrigin.x;
}
return rect;
}
// MARK: - Drawing
- (void)drawRect:(CPRect)aRect
{
if (!_image) return;
var rect = [self imageRectInRuler];
// Only draw if visible
if (CPRectIntersectsRect(rect, aRect))
{
// Visual feedback for dragging
var opacity = _dragging ? 0.5 : 1.0;
[_image drawInRect:rect fromRect:CPZeroRect operation:CPCompositeSourceOver fraction:opacity];
}
}
// MARK: - Event Handling
- (BOOL)trackMouse:(CPEvent)anEvent adding:(BOOL)adding
{
if (!adding && !_movable) return NO;
var clientView = [_ruler clientView],
delegate = clientView; // The client view acts as the delegate usually
// Delegate Check: Should we move/add?
if (adding)
{
if ([delegate respondsToSelector:@selector(rulerView:shouldAddMarker:)] &&
![delegate rulerView:_ruler shouldAddMarker:self])
return NO;
}
else
{
if ([delegate respondsToSelector:@selector(rulerView:shouldMoveMarker:)] &&
![delegate rulerView:_ruler shouldMoveMarker:self])
return NO;
}
// If adding, we ensure the marker is in the array so it draws
if (adding)
[_ruler addMarker:self];
[self setDragging:YES];
var type = [anEvent type],
originalLocation = _markerLocation;
// Start Event Loop
while (type !== CPLeftMouseUp)
{
// 1. Calculate new location
var locationInWindow = [anEvent locationInWindow],
pointInClient = [clientView convertPoint:locationInWindow fromView:nil],
newLocation = ([_ruler orientation] === CPHorizontalRuler) ? pointInClient.x : pointInClient.y;
// 2. Delegate: Will Move? (Snap logic usually happens here)
if ([delegate respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)])
{
newLocation = [delegate rulerView:_ruler willMoveMarker:self toLocation:newLocation];
}
// 3. Update Location
[self setMarkerLocation:newLocation];
// 4. Handle "Tearing off" (Removal visual feedback)
// Check if mouse is far outside the ruler's bounds
var pointInRuler = [_ruler convertPoint:locationInWindow fromView:nil];
var rulerBounds = [_ruler bounds];
// Expand bounds slightly for tolerance
var expandedBounds = CPMakeRect(rulerBounds.origin.x - 20, rulerBounds.origin.y - 20,
rulerBounds.size.width + 40, rulerBounds.size.height + 40);
var isFarAway = !CPPointInRect(pointInRuler, expandedBounds);
// You might change the cursor here to a "poof" cursor if isFarAway && _removable
// 5. Get next event
anEvent = [[CPApp currentEvent] window] ? [[CPApp currentEvent] window] : [CPApp keyWindow]; // fallback
anEvent = [CPApp nextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask
untilDate:[CPDate distantFuture]
inMode:CPDefaultRunLoopMode
dequeue:YES];
type = [anEvent type];
}
[self setDragging:NO];
// Finalize
var pointInRuler = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil],
rulerBounds = [_ruler bounds],
expandedBounds = CPMakeRect(rulerBounds.origin.x - 10, rulerBounds.origin.y - 10,
rulerBounds.size.width + 20, rulerBounds.size.height + 20),
shouldRemove = _removable && !CPPointInRect(pointInRuler, expandedBounds);
if (shouldRemove)
{
var allowed = YES;
if ([delegate respondsToSelector:@selector(rulerView:shouldRemoveMarker:)])
allowed = [delegate rulerView:_ruler shouldRemoveMarker:self];
if (allowed)
{
[_ruler removeMarker:self];
// Don't call didMove or didAdd if removed
return YES;
}
}
// Success notification
if (adding)
{
if ([delegate respondsToSelector:@selector(rulerView:didAddMarker:)])
[delegate rulerView:_ruler didAddMarker:self];
}
else
{
if ([delegate respondsToSelector:@selector(rulerView:didMoveMarker:)])
[delegate rulerView:_ruler didMoveMarker:self];
}
return YES;
}
@end