mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-31 20:37:36 +00:00
Merge remote-tracking branch 'upstream/main' into Aristo3
This commit is contained in:
commit
3ad080b788
142 files changed
+14126
-3630
No files matched your search
@@ -25,10 +25,10 @@
|
||||
*/
|
||||
|
||||
@import "CPText.j"
|
||||
@import "CPTextContainer.j"
|
||||
@import "CGContext.j"
|
||||
@import "CPTypesetter.j"
|
||||
@import "CPFont.j"
|
||||
@import "CPCompatibility.j"
|
||||
|
||||
@global _MakeRangeFromAbs
|
||||
@global document
|
||||
@@ -298,9 +298,10 @@ _oncontextmenuhandler = function () { return false; };
|
||||
|
||||
[_typesetter layoutGlyphsInLayoutManager:self startingAtGlyphIndex:startIndex maxNumberOfLineFragments:-1 nextGlyphIndex:nil];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
[self _cleanUpDOM];
|
||||
#endif
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
[self _cleanUpDOM];
|
||||
}
|
||||
|
||||
_isValidatingLayoutAndGlyphs = NO;
|
||||
}
|
||||
@@ -1124,6 +1125,7 @@ var _objectsInRange = function(aList, aRange)
|
||||
|
||||
BOOL _isInvalid;
|
||||
BOOL _isLast;
|
||||
float _wordSpacing;
|
||||
CGRect _fragmentRect;
|
||||
CGRect _usedRect;
|
||||
CGPoint _location;
|
||||
@@ -1146,7 +1148,8 @@ var _objectsInRange = function(aList, aRange)
|
||||
if (!aString || aString.length === 0)
|
||||
return nil;
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (!CPDOMAvailable) return nil;
|
||||
|
||||
var style,
|
||||
span = document.createElement("span");
|
||||
|
||||
@@ -1160,6 +1163,10 @@ var _objectsInRange = function(aList, aRange)
|
||||
style.whiteSpace = "pre";
|
||||
style.backgroundColor = "transparent";
|
||||
style.font = [aFont cssString];
|
||||
|
||||
// A justified line widens its spaces; the typesetter has added the same width to their advancements.
|
||||
if (_wordSpacing)
|
||||
style.wordSpacing = _wordSpacing + "px";
|
||||
|
||||
if (aUnderline)
|
||||
{
|
||||
@@ -1197,9 +1204,6 @@ var _objectsInRange = function(aList, aRange)
|
||||
span.textContent = aString;
|
||||
|
||||
return span;
|
||||
#else
|
||||
return nil;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (id)initWithRange:(CPRange)aRange textContainer:(CPTextContainer)aContainer textStorage:(CPTextStorage)textStorage
|
||||
@@ -1215,6 +1219,7 @@ var _objectsInRange = function(aList, aRange)
|
||||
_range = CPMakeRangeCopy(aRange);
|
||||
_textContainer = aContainer;
|
||||
_isInvalid = NO;
|
||||
_wordSpacing = 0.0;
|
||||
_runs = [];
|
||||
_glyphsFrames = [];
|
||||
_glyphsOffsets = [];
|
||||
@@ -1495,6 +1500,9 @@ var _objectsInRange = function(aList, aRange)
|
||||
if (!oldFragmentRuns || !newFragmentRuns || oldFragmentRuns.length !== newFragmentRuns.length)
|
||||
return NO;
|
||||
|
||||
if (newLineFragment._wordSpacing !== _wordSpacing)
|
||||
return NO;
|
||||
|
||||
var l = oldFragmentRuns.length;
|
||||
|
||||
for (var i = 0; i < l; i++)
|
||||
|
||||
@@ -106,6 +106,24 @@ var kDefaultTabInterval = 28.0;
|
||||
|
||||
// MARK: - CPParagraphStyle Implementation
|
||||
|
||||
/*
|
||||
Returns YES if both arrays hold the same text lists, in the same order. A text list is
|
||||
identified by the object itself, so the lists are compared by identity.
|
||||
*/
|
||||
var _CPParagraphStyleTextListsAreIdentical = function(someLists, otherLists)
|
||||
{
|
||||
var count = someLists ? someLists.length : 0;
|
||||
|
||||
if (count !== (otherLists ? otherLists.length : 0))
|
||||
return NO;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
if (someLists[i] !== otherLists[i])
|
||||
return NO;
|
||||
|
||||
return YES;
|
||||
};
|
||||
|
||||
var _sharedDefaultParagraphStyle = nil;
|
||||
|
||||
@implementation CPParagraphStyle : CPObject
|
||||
@@ -124,6 +142,7 @@ var _sharedDefaultParagraphStyle = nil;
|
||||
float _paragraphSpacingBefore @accessors(readonly, property=paragraphSpacingBefore);
|
||||
float _defaultTabInterval @accessors(readonly, property=defaultTabInterval);
|
||||
CPArray _tabStops @accessors(readonly, property=tabStops);
|
||||
CPArray _textLists @accessors(readonly, property=textLists);
|
||||
}
|
||||
|
||||
+ (CPParagraphStyle)defaultParagraphStyle
|
||||
@@ -168,6 +187,8 @@ var _sharedDefaultParagraphStyle = nil;
|
||||
[_tabStops addObject:[[CPTextTab alloc] initWithType:CPLeftTextAlignment
|
||||
location:i * kDefaultTabInterval]];
|
||||
}
|
||||
|
||||
_textLists = [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -190,6 +211,7 @@ var _sharedDefaultParagraphStyle = nil;
|
||||
_paragraphSpacingBefore = [other paragraphSpacingBefore];
|
||||
_defaultTabInterval = [other defaultTabInterval];
|
||||
_tabStops = [[other tabStops] copy];
|
||||
_textLists = [[other textLists] copy] || [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -229,6 +251,7 @@ var _sharedDefaultParagraphStyle = nil;
|
||||
_paragraphSpacingBefore = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacingBefore"];
|
||||
_defaultTabInterval = [aCoder decodeFloatForKey:@"CPParagraphStyleDefaultTabInterval"];
|
||||
_tabStops = [aCoder decodeObjectForKey:@"CPParagraphStyleTabStops"];
|
||||
_textLists = [aCoder decodeObjectForKey:@"CPParagraphStyleTextLists"] || [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -249,6 +272,9 @@ var _sharedDefaultParagraphStyle = nil;
|
||||
[aCoder encodeFloat:_paragraphSpacingBefore forKey:@"CPParagraphStyleParagraphSpacingBefore"];
|
||||
[aCoder encodeFloat:_defaultTabInterval forKey:@"CPParagraphStyleDefaultTabInterval"];
|
||||
[aCoder encodeObject:_tabStops forKey:@"CPParagraphStyleTabStops"];
|
||||
|
||||
if ([_textLists count])
|
||||
[aCoder encodeObject:_textLists forKey:@"CPParagraphStyleTextLists"];
|
||||
}
|
||||
|
||||
// MARK: - Equality
|
||||
@@ -265,7 +291,8 @@ var _sharedDefaultParagraphStyle = nil;
|
||||
_tailIndent === [other tailIndent] &&
|
||||
_firstLineHeadIndent === [other firstLineHeadIndent] &&
|
||||
_lineBreakMode === [other lineBreakMode] &&
|
||||
[_tabStops isEqualToArray:[other tabStops]];
|
||||
[_tabStops isEqualToArray:[other tabStops]] &&
|
||||
_CPParagraphStyleTextListsAreIdentical(_textLists, [other textLists]);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -377,6 +404,14 @@ var _sharedDefaultParagraphStyle = nil;
|
||||
_tabStops = [newTabStops mutableCopy];
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the text lists that the paragraph belongs to, outermost first.
|
||||
*/
|
||||
- (void)setTextLists:(CPArray)someTextLists
|
||||
{
|
||||
_textLists = [someTextLists copy] || [];
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(CPZone)aZone
|
||||
{
|
||||
// Return an immutable copy
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* CPRulerMarker.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/CPObject.j>
|
||||
|
||||
@import "CGGeometry.j"
|
||||
@import "CPEvent.j"
|
||||
@import "CPGraphicsContext.j"
|
||||
@import "CPImage.j"
|
||||
|
||||
@class CPRulerView
|
||||
|
||||
@global CPApp
|
||||
@global CPHorizontalRuler
|
||||
|
||||
|
||||
/*!
|
||||
Distance, in points, that the pointer must move beyond the ruler before a removable
|
||||
marker is removed when it is released.
|
||||
*/
|
||||
var CPRulerMarkerRemovalDistance = 0.0;
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPRulerMarker
|
||||
|
||||
A CPRulerMarker shows a location on a CPRulerView, such as a tab stop or a paragraph
|
||||
indent. The marker is drawn by its ruler view as an image, positioned so that the
|
||||
image's \c imageOrigin lies at the marker's location on the ruler's baseline. The
|
||||
marker's location is expressed in the coordinate system of the ruler's client view.
|
||||
|
||||
The user can drag a movable marker along the ruler, and drag a removable marker off the
|
||||
ruler to remove it. The ruler's client view is consulted through the CPRulerView client
|
||||
methods before and after each change.
|
||||
*/
|
||||
@implementation CPRulerMarker : CPObject
|
||||
{
|
||||
CPRulerView _ruler;
|
||||
float _markerLocation;
|
||||
CPImage _image;
|
||||
CGPoint _imageOrigin;
|
||||
BOOL _isMovable;
|
||||
BOOL _isRemovable;
|
||||
BOOL _isDragging;
|
||||
BOOL _isOffRuler;
|
||||
id _representedObject;
|
||||
|
||||
float _trackingStartLocation;
|
||||
float _trackingStartPoint;
|
||||
float _trackingStartCrossPoint;
|
||||
float _trackingCrossOffset;
|
||||
BOOL _trackingIsAdding;
|
||||
BOOL _trackingMoved;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes a marker for \c aRulerView at \c aLocation, drawn with \c anImage. The
|
||||
marker is associated with the ruler view, but not added to it. A new marker is movable
|
||||
and not removable.
|
||||
@param aRulerView the ruler view the marker belongs to
|
||||
@param aLocation the marker's location, in the client view's coordinate system
|
||||
@param anImage the image drawn for the marker
|
||||
@param anImageOrigin the point in the image, measured from its top left corner, placed
|
||||
at the marker's location on the ruler's baseline
|
||||
@return the initialized marker
|
||||
*/
|
||||
- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation image:(CPImage)anImage imageOrigin:(CGPoint)anImageOrigin
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_ruler = aRulerView;
|
||||
_markerLocation = aLocation;
|
||||
_image = anImage;
|
||||
_imageOrigin = CGPointMakeCopy(anImageOrigin);
|
||||
_isMovable = YES;
|
||||
_isRemovable = NO;
|
||||
_isDragging = NO;
|
||||
_isOffRuler = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// MARK: Getting the Ruler View
|
||||
|
||||
/*!
|
||||
Returns the ruler view the marker belongs to.
|
||||
*/
|
||||
- (CPRulerView)ruler
|
||||
{
|
||||
return _ruler;
|
||||
}
|
||||
|
||||
// MARK: Setting the Image
|
||||
|
||||
/*!
|
||||
Returns the image drawn for the marker.
|
||||
*/
|
||||
- (CPImage)image
|
||||
{
|
||||
return _image;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the image drawn for the marker.
|
||||
@param anImage the new image
|
||||
*/
|
||||
- (void)setImage:(CPImage)anImage
|
||||
{
|
||||
if (_image === anImage)
|
||||
return;
|
||||
|
||||
_image = anImage;
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the point in the image, measured from its top left corner, that is placed at
|
||||
the marker's location on the ruler's baseline.
|
||||
*/
|
||||
- (CGPoint)imageOrigin
|
||||
{
|
||||
return _imageOrigin;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the point in the image, measured from its top left corner, that is placed at the
|
||||
marker's location on the ruler's baseline.
|
||||
@param aPoint the new image origin
|
||||
*/
|
||||
- (void)setImageOrigin:(CGPoint)aPoint
|
||||
{
|
||||
_imageOrigin = CGPointMakeCopy(aPoint);
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the rectangle that the marker's image occupies, in the ruler view's coordinate
|
||||
system.
|
||||
*/
|
||||
- (CGRect)imageRectInRuler
|
||||
{
|
||||
var imageSize = [_image size] || CGSizeMakeZero(),
|
||||
point = [_ruler _rulerPointForLocation:_markerLocation],
|
||||
baseline = [_ruler baselineLocation];
|
||||
|
||||
// Whole-point positions keep every marker image sharp and identical, wherever it lies on the ruler.
|
||||
// While the user drags the marker outward, it follows the pointer across the ruler.
|
||||
var crossPosition = baseline + (_isDragging ? _trackingCrossOffset : 0.0);
|
||||
|
||||
if ([_ruler orientation] === CPHorizontalRuler)
|
||||
return CGRectMake(ROUND(point - _imageOrigin.x), ROUND(crossPosition - _imageOrigin.y), imageSize.width, imageSize.height);
|
||||
|
||||
return CGRectMake(ROUND(crossPosition - _imageOrigin.x), ROUND(point - _imageOrigin.y), imageSize.width, imageSize.height);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the extent of the marker's image on the marker side of the ruler's baseline:
|
||||
above the baseline on a horizontal ruler, and to its left on a vertical ruler.
|
||||
*/
|
||||
- (float)thicknessRequiredInRuler
|
||||
{
|
||||
return [_ruler orientation] === CPHorizontalRuler ? _imageOrigin.y : _imageOrigin.x;
|
||||
}
|
||||
|
||||
// MARK: Setting Movability
|
||||
|
||||
/*!
|
||||
Returns \c YES if the user can drag the marker along the ruler.
|
||||
*/
|
||||
- (BOOL)isMovable
|
||||
{
|
||||
return _isMovable;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets whether the user can drag the marker along the ruler.
|
||||
@param shouldBeMovable \c YES to let the user move the marker
|
||||
*/
|
||||
- (void)setMovable:(BOOL)shouldBeMovable
|
||||
{
|
||||
_isMovable = shouldBeMovable;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns \c YES if the user can remove the marker by dragging it off the ruler.
|
||||
*/
|
||||
- (BOOL)isRemovable
|
||||
{
|
||||
return _isRemovable;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets whether the user can remove the marker by dragging it off the ruler.
|
||||
@param shouldBeRemovable \c YES to let the user remove the marker
|
||||
*/
|
||||
- (void)setRemovable:(BOOL)shouldBeRemovable
|
||||
{
|
||||
_isRemovable = shouldBeRemovable;
|
||||
}
|
||||
|
||||
// MARK: Setting the Location
|
||||
|
||||
/*!
|
||||
Returns the marker's location, in the client view's coordinate system.
|
||||
*/
|
||||
- (float)markerLocation
|
||||
{
|
||||
return _markerLocation;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the marker's location, in the client view's coordinate system.
|
||||
@param aLocation the new location
|
||||
*/
|
||||
- (void)setMarkerLocation:(float)aLocation
|
||||
{
|
||||
if (_markerLocation === aLocation)
|
||||
return;
|
||||
|
||||
_markerLocation = aLocation;
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
// MARK: Setting the Represented Object
|
||||
|
||||
/*!
|
||||
Returns the object the marker represents, such as a CPTextTab.
|
||||
*/
|
||||
- (id)representedObject
|
||||
{
|
||||
return _representedObject;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the object the marker represents.
|
||||
@param anObject the represented object
|
||||
*/
|
||||
- (void)setRepresentedObject:(id)anObject
|
||||
{
|
||||
_representedObject = anObject;
|
||||
}
|
||||
|
||||
// MARK: Drawing and Event Handling
|
||||
|
||||
/*!
|
||||
Returns \c YES while the user is dragging the marker.
|
||||
*/
|
||||
- (BOOL)isDragging
|
||||
{
|
||||
return _isDragging;
|
||||
}
|
||||
|
||||
/*!
|
||||
Draws the marker's image if it lies within \c aRect, in the ruler view's coordinate
|
||||
system. While a removable marker is dragged off the ruler, it is drawn faded.
|
||||
@param aRect the rectangle being drawn
|
||||
*/
|
||||
- (void)drawRect:(CGRect)aRect
|
||||
{
|
||||
if (!_image || [_image loadStatus] !== CPImageLoadStatusCompleted)
|
||||
return;
|
||||
|
||||
var imageRect = [self imageRectInRuler];
|
||||
|
||||
if (!CGRectIntersectsRect(imageRect, aRect))
|
||||
return;
|
||||
|
||||
var context = [[CPGraphicsContext currentContext] graphicsPort];
|
||||
|
||||
CGContextSaveGState(context);
|
||||
|
||||
// Off the ruler's outer edge the marker follows the pointer and is clipped by the ruler.
|
||||
// Off its inner edge it stays on its line, so it is drawn faded instead.
|
||||
if (_isOffRuler && _trackingCrossOffset === 0.0)
|
||||
CGContextSetAlpha(context, 0.4);
|
||||
|
||||
CGContextDrawImage(context, imageRect, _image);
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
|
||||
/*!
|
||||
Starts tracking the user's manipulation of the marker, from the mouse-down event
|
||||
\c anEvent. Tracking continues until the mouse button is released.
|
||||
|
||||
If \c isAdding is \c YES, the marker is not yet in the ruler: it is added when the user
|
||||
releases it over the ruler, if the client view agrees. Otherwise the marker is moved,
|
||||
and removed if it is removable and released away from the ruler.
|
||||
@param anEvent the mouse-down event that started the manipulation
|
||||
@param isAdding \c YES if the marker is being added to the ruler
|
||||
@return \c YES if tracking started, or \c NO if the marker cannot be moved or removed,
|
||||
or the client view refused
|
||||
*/
|
||||
- (BOOL)trackMouse:(CPEvent)anEvent adding:(BOOL)isAdding
|
||||
{
|
||||
if (!isAdding && !_isMovable && !_isRemovable)
|
||||
return NO;
|
||||
|
||||
if (isAdding && ![_ruler _clientShouldAddMarker:self])
|
||||
return NO;
|
||||
|
||||
if (!isAdding && _isMovable && ![_ruler _clientShouldMoveMarker:self])
|
||||
return NO;
|
||||
|
||||
_trackingIsAdding = isAdding;
|
||||
_trackingMoved = NO;
|
||||
_trackingStartLocation = _markerLocation;
|
||||
_trackingStartPoint = [self _axisPointForEvent:anEvent];
|
||||
_trackingStartCrossPoint = [self _crossPointForEvent:anEvent];
|
||||
_trackingCrossOffset = 0.0;
|
||||
_isDragging = YES;
|
||||
_isOffRuler = NO;
|
||||
|
||||
if (isAdding)
|
||||
{
|
||||
_markerLocation = [_ruler _clientWillAddMarker:self atLocation:_markerLocation];
|
||||
[_ruler _setTrackedMarker:self];
|
||||
}
|
||||
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
|
||||
[CPApp setTarget:self
|
||||
selector:@selector(_trackMouseWithEvent:)
|
||||
forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask
|
||||
untilDate:nil
|
||||
inMode:nil
|
||||
dequeue:YES];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (void)_trackMouseWithEvent:(CPEvent)anEvent
|
||||
{
|
||||
var type = [anEvent type];
|
||||
|
||||
if (type === CPLeftMouseDragged)
|
||||
{
|
||||
_trackingMoved = YES;
|
||||
_isOffRuler = [self _isEventOffRuler:anEvent] && (_trackingIsAdding || _isRemovable);
|
||||
|
||||
// As in Cocoa, the marker stays on its line while the pointer is on the ruler, and follows
|
||||
// the pointer out over the ruler's outer edge, where the ruler clips it.
|
||||
_trackingCrossOffset = (_trackingIsAdding || _isRemovable) ? MIN(0.0, [self _crossPointForEvent:anEvent] - _trackingStartCrossPoint) : 0.0;
|
||||
|
||||
if (_trackingIsAdding || _isMovable)
|
||||
{
|
||||
var proposedLocation = _trackingStartLocation + [self _axisPointForEvent:anEvent] - _trackingStartPoint;
|
||||
|
||||
_markerLocation = _trackingIsAdding ? [_ruler _clientWillAddMarker:self atLocation:proposedLocation]
|
||||
: [_ruler _clientWillMoveMarker:self toLocation:proposedLocation];
|
||||
}
|
||||
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
|
||||
[CPApp setTarget:self
|
||||
selector:@selector(_trackMouseWithEvent:)
|
||||
forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask
|
||||
untilDate:nil
|
||||
inMode:nil
|
||||
dequeue:YES];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type !== CPLeftMouseUp)
|
||||
return;
|
||||
|
||||
var wasOffRuler = _isOffRuler;
|
||||
|
||||
_trackingCrossOffset = 0.0;
|
||||
|
||||
_isDragging = NO;
|
||||
_isOffRuler = NO;
|
||||
|
||||
if (_trackingIsAdding)
|
||||
{
|
||||
[_ruler _setTrackedMarker:nil];
|
||||
|
||||
if (!wasOffRuler)
|
||||
{
|
||||
[_ruler addMarker:self];
|
||||
[_ruler _clientDidAddMarker:self];
|
||||
}
|
||||
}
|
||||
else if (wasOffRuler && [_ruler _clientShouldRemoveMarker:self])
|
||||
{
|
||||
[_ruler removeMarker:self];
|
||||
[_ruler _clientDidRemoveMarker:self];
|
||||
}
|
||||
else if (_trackingMoved)
|
||||
{
|
||||
if (wasOffRuler)
|
||||
_markerLocation = _trackingStartLocation;
|
||||
|
||||
if (_markerLocation !== _trackingStartLocation)
|
||||
[_ruler _clientDidMoveMarker:self];
|
||||
}
|
||||
|
||||
[_ruler setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (float)_crossPointForEvent:(CPEvent)anEvent
|
||||
{
|
||||
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil];
|
||||
|
||||
return [_ruler orientation] === CPHorizontalRuler ? point.y : point.x;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (float)_axisPointForEvent:(CPEvent)anEvent
|
||||
{
|
||||
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil];
|
||||
|
||||
return [_ruler orientation] === CPHorizontalRuler ? point.x : point.y;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (BOOL)_isEventOffRuler:(CPEvent)anEvent
|
||||
{
|
||||
// Only distance across the ruler counts. Dragging past either end of the ruler moves the marker.
|
||||
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil],
|
||||
bounds = [_ruler bounds];
|
||||
|
||||
if ([_ruler orientation] === CPHorizontalRuler)
|
||||
return point.y < CGRectGetMinY(bounds) - CPRulerMarkerRemovalDistance || point.y > CGRectGetMaxY(bounds) + CPRulerMarkerRemovalDistance;
|
||||
|
||||
return point.x < CGRectGetMinX(bounds) - CPRulerMarkerRemovalDistance || point.x > CGRectGetMaxX(bounds) + CPRulerMarkerRemovalDistance;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
var CPRulerMarkerRulerKey = @"CPRulerMarkerRulerKey",
|
||||
CPRulerMarkerLocationKey = @"CPRulerMarkerLocationKey",
|
||||
CPRulerMarkerImageKey = @"CPRulerMarkerImageKey",
|
||||
CPRulerMarkerImageOriginKey = @"CPRulerMarkerImageOriginKey",
|
||||
CPRulerMarkerMovableKey = @"CPRulerMarkerMovableKey",
|
||||
CPRulerMarkerRemovableKey = @"CPRulerMarkerRemovableKey",
|
||||
CPRulerMarkerRepresentedObjectKey = @"CPRulerMarkerRepresentedObjectKey";
|
||||
|
||||
@implementation CPRulerMarker (CPCoding)
|
||||
|
||||
/*!
|
||||
Initializes a marker from an archive.
|
||||
*/
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_ruler = [aCoder decodeObjectForKey:CPRulerMarkerRulerKey];
|
||||
_markerLocation = [aCoder decodeFloatForKey:CPRulerMarkerLocationKey];
|
||||
_image = [aCoder decodeObjectForKey:CPRulerMarkerImageKey];
|
||||
_imageOrigin = [aCoder decodePointForKey:CPRulerMarkerImageOriginKey];
|
||||
_isMovable = [aCoder decodeBoolForKey:CPRulerMarkerMovableKey];
|
||||
_isRemovable = [aCoder decodeBoolForKey:CPRulerMarkerRemovableKey];
|
||||
_representedObject = [aCoder decodeObjectForKey:CPRulerMarkerRepresentedObjectKey];
|
||||
_isDragging = NO;
|
||||
_isOffRuler = NO;
|
||||
_trackingCrossOffset = 0.0;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Archives the marker. The ruler is archived conditionally.
|
||||
*/
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeConditionalObject:_ruler forKey:CPRulerMarkerRulerKey];
|
||||
[aCoder encodeFloat:_markerLocation forKey:CPRulerMarkerLocationKey];
|
||||
[aCoder encodeObject:_image forKey:CPRulerMarkerImageKey];
|
||||
[aCoder encodePoint:_imageOrigin forKey:CPRulerMarkerImageOriginKey];
|
||||
[aCoder encodeBool:_isMovable forKey:CPRulerMarkerMovableKey];
|
||||
[aCoder encodeBool:_isRemovable forKey:CPRulerMarkerRemovableKey];
|
||||
[aCoder encodeObject:_representedObject forKey:CPRulerMarkerRepresentedObjectKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* CPTextAttachment.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 25/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* 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/CPObject.j>
|
||||
@import <Foundation/CPData.j>
|
||||
@import <Foundation/CPNotificationCenter.j>
|
||||
|
||||
@import "CPImage.j"
|
||||
@import "CPImageView.j"
|
||||
|
||||
/*
|
||||
Posted when an attachment's displayed size changes, for example when its image has loaded.
|
||||
A text view that contains the attachment lays its text out again.
|
||||
*/
|
||||
_CPTextAttachmentDidChangeSizeNotification = @"_CPTextAttachmentDidChangeSizeNotification";
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPTextAttachment
|
||||
|
||||
A CPTextAttachment holds an image in rich text, as NSTextAttachment does in Cocoa. The text
|
||||
holds the attachment as one attachment character (CPAttachmentCharacter), whose
|
||||
CPAttachmentAttributeName attribute is the attachment. The attachment is edited, copied and
|
||||
undone as that one character.
|
||||
|
||||
The attachment keeps the image's data and type, and shows the image in a view. As in TextKit
|
||||
2, an attachment is drawn by a view, not by a cell. The text system lays the view out as the
|
||||
character, at the view's size, with its bottom on the baseline. That size is the attachment's
|
||||
bounds, or the image's natural size, scaled down to fit the width of the text, as in TextEdit.
|
||||
*/
|
||||
@implementation CPTextAttachment : CPObject
|
||||
{
|
||||
CPData _contents;
|
||||
CPString _fileType;
|
||||
CPImage _image;
|
||||
CGRect _bounds;
|
||||
|
||||
CPImageView _imageView;
|
||||
float _maximumWidth;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initialises an attachment with image data.
|
||||
@param contentData the image's data
|
||||
@param aType the image's type, as a MIME type such as \c @"image/png", or a uniform type
|
||||
identifier such as \c @"public.png"
|
||||
*/
|
||||
- (id)initWithData:(CPData)contentData ofType:(CPString)aType
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_contents = contentData;
|
||||
_fileType = aType;
|
||||
_bounds = CGRectMakeZero();
|
||||
_maximumWidth = 0.0;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithData:nil ofType:nil];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns an attachment made from a data URL, such as the browser's FileReader produces.
|
||||
*/
|
||||
+ (CPTextAttachment)_attachmentWithDataURL:(CPString)aDataURL
|
||||
{
|
||||
var match = /^data:([^;,]+)(;base64)?,(.*)$/.exec(aDataURL || "");
|
||||
|
||||
if (!match)
|
||||
return nil;
|
||||
|
||||
var data = match[2] ? [CPData dataWithBase64:match[3]] : [CPData dataWithRawString:decodeURIComponent(match[3])];
|
||||
|
||||
return [[self alloc] initWithData:data ofType:match[1]];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the image's data.
|
||||
*/
|
||||
- (CPData)contents
|
||||
{
|
||||
return _contents;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the image's data. The image is made again from the data.
|
||||
*/
|
||||
- (void)setContents:(CPData)contentData
|
||||
{
|
||||
_contents = contentData;
|
||||
[self _setImage:nil];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the image's type.
|
||||
*/
|
||||
- (CPString)fileType
|
||||
{
|
||||
return _fileType;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the image's type.
|
||||
*/
|
||||
- (void)setFileType:(CPString)aType
|
||||
{
|
||||
_fileType = aType;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the image. It is made from the attachment's data the first time it is needed.
|
||||
*/
|
||||
- (CPImage)image
|
||||
{
|
||||
if (!_image && _contents)
|
||||
[self _setImage:[[CPImage alloc] initWithContentsOfFile:[self _dataURL]]];
|
||||
|
||||
return _image;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the image. An image set this way has no data of its own unless it was made from a data
|
||||
URL, so an attachment that must be archived or copied should be made with
|
||||
-initWithData:ofType:.
|
||||
*/
|
||||
- (void)setImage:(CPImage)anImage
|
||||
{
|
||||
[self _setImage:anImage];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the attachment's bounds in the text. With a zero size, the attachment takes the
|
||||
image's natural size.
|
||||
*/
|
||||
- (CGRect)bounds
|
||||
{
|
||||
return CGRectMakeCopy(_bounds);
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the attachment's bounds in the text.
|
||||
*/
|
||||
- (void)setBounds:(CGRect)aRect
|
||||
{
|
||||
_bounds = CGRectMakeCopy(aRect);
|
||||
[self _updateViewSize];
|
||||
}
|
||||
|
||||
// MARK: Display
|
||||
|
||||
/* @ignore
|
||||
Returns the view that shows the attachment in the text. The text system lays it out as the
|
||||
attachment character.
|
||||
*/
|
||||
- (CPView)_attachmentView
|
||||
{
|
||||
if (!_imageView)
|
||||
{
|
||||
_imageView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
|
||||
[_imageView setImageScaling:CPImageScaleAxesIndependently];
|
||||
[_imageView setImage:[self image]];
|
||||
[self _updateViewSize];
|
||||
}
|
||||
|
||||
return _imageView;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Sets the widest the attachment may be shown, which is the width of the text. Returns YES if
|
||||
the displayed size changes.
|
||||
*/
|
||||
- (BOOL)_setMaximumWidth:(float)aWidth
|
||||
{
|
||||
if (_maximumWidth === aWidth)
|
||||
return NO;
|
||||
|
||||
_maximumWidth = aWidth;
|
||||
|
||||
return [self _updateViewSize];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the size at which the attachment is shown: its bounds, or the image's natural size,
|
||||
scaled down in proportion to fit the maximum width. The size is zero until the image has
|
||||
loaded.
|
||||
*/
|
||||
- (CGSize)_displaySize
|
||||
{
|
||||
var size = CGSizeMakeCopy(_bounds.size);
|
||||
|
||||
if (!size.width || !size.height)
|
||||
{
|
||||
var image = [self image];
|
||||
|
||||
if (!image || [image loadStatus] !== CPImageLoadStatusCompleted)
|
||||
return CGSizeMakeZero();
|
||||
|
||||
size = CGSizeMakeCopy([image size]);
|
||||
}
|
||||
|
||||
if (_maximumWidth > 0.0 && size.width > _maximumWidth)
|
||||
{
|
||||
size.height = ROUND(size.height * _maximumWidth / size.width);
|
||||
size.width = _maximumWidth;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Gives the view the displayed size. Returns YES if the size changes.
|
||||
*/
|
||||
- (BOOL)_updateViewSize
|
||||
{
|
||||
if (!_imageView)
|
||||
return NO;
|
||||
|
||||
var size = [self _displaySize];
|
||||
|
||||
if (CGSizeEqualToSize(size, [_imageView frameSize]))
|
||||
return NO;
|
||||
|
||||
[_imageView setFrameSize:size];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Sets the image, and waits for it to load when it has not yet loaded.
|
||||
*/
|
||||
- (void)_setImage:(CPImage)anImage
|
||||
{
|
||||
if (_image)
|
||||
[[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:_image];
|
||||
|
||||
_image = anImage;
|
||||
|
||||
if (_image && [_image loadStatus] !== CPImageLoadStatusCompleted)
|
||||
{
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_imageDidLoad:) name:CPImageDidLoadNotification object:_image];
|
||||
[_image load];
|
||||
}
|
||||
|
||||
if (_imageView)
|
||||
{
|
||||
[_imageView setImage:_image];
|
||||
|
||||
if ([self _updateViewSize])
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:_CPTextAttachmentDidChangeSizeNotification object:self];
|
||||
}
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
The image's natural size is known once it has loaded, so the view takes its size, and the
|
||||
text that contains the attachment is laid out again.
|
||||
*/
|
||||
- (void)_imageDidLoad:(CPNotification)aNotification
|
||||
{
|
||||
[[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:_image];
|
||||
|
||||
if (_imageView)
|
||||
[_imageView setImage:_image];
|
||||
|
||||
if ([self _updateViewSize])
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:_CPTextAttachmentDidChangeSizeNotification object:self];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the attachment's data as a data URL, which the browser loads as an image.
|
||||
*/
|
||||
- (CPString)_dataURL
|
||||
{
|
||||
return "data:" + [self _MIMEType] + ";base64," + [_contents base64];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the image's MIME type. A uniform type identifier is mapped to its MIME type.
|
||||
*/
|
||||
- (CPString)_MIMEType
|
||||
{
|
||||
var type = _fileType || @"image/png";
|
||||
|
||||
if (type.indexOf("/") !== -1)
|
||||
return type;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case @"public.jpeg":
|
||||
return @"image/jpeg";
|
||||
|
||||
case @"com.compuserve.gif":
|
||||
return @"image/gif";
|
||||
|
||||
case @"public.svg-image":
|
||||
return @"image/svg+xml";
|
||||
|
||||
case @"org.webmproject.webp":
|
||||
return @"image/webp";
|
||||
|
||||
default:
|
||||
return @"image/png";
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPTextAttachmentContentsKey = @"CPTextAttachmentContents",
|
||||
CPTextAttachmentFileTypeKey = @"CPTextAttachmentFileType",
|
||||
CPTextAttachmentBoundsKey = @"CPTextAttachmentBounds";
|
||||
|
||||
@implementation CPTextAttachment (CPCoding)
|
||||
|
||||
/*!
|
||||
Decodes the attachment. The image is made again from the archived data.
|
||||
*/
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
var contentsString = [aCoder decodeObjectForKey:CPTextAttachmentContentsKey];
|
||||
|
||||
self = [self initWithData:contentsString ? [CPData dataWithBase64:contentsString] : nil
|
||||
ofType:[aCoder decodeObjectForKey:CPTextAttachmentFileTypeKey]];
|
||||
|
||||
if (self && [aCoder containsValueForKey:CPTextAttachmentBoundsKey])
|
||||
_bounds = [aCoder decodeRectForKey:CPTextAttachmentBoundsKey];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Encodes the attachment's data, type and bounds. The view is not encoded.
|
||||
*/
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
if (_contents)
|
||||
[aCoder encodeObject:[_contents base64] forKey:CPTextAttachmentContentsKey];
|
||||
|
||||
if (_fileType)
|
||||
[aCoder encodeObject:_fileType forKey:CPTextAttachmentFileTypeKey];
|
||||
|
||||
if (!CGRectIsEmpty(_bounds))
|
||||
[aCoder encodeRect:_bounds forKey:CPTextAttachmentBoundsKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -112,7 +112,8 @@ CPLineMovesUp = 4;
|
||||
|
||||
- (void)_init
|
||||
{
|
||||
_lineFragmentPadding = 0.0;
|
||||
// Cocoa's default: room at each end of a line fragment, so that text does not touch the container's edges.
|
||||
_lineFragmentPadding = 5.0;
|
||||
|
||||
_layoutManager = [[CPLayoutManager alloc] init];
|
||||
[_layoutManager addTextContainer:self];
|
||||
@@ -129,6 +130,9 @@ CPLineMovesUp = 4;
|
||||
|
||||
if (oldSize.width != _size.width)
|
||||
{
|
||||
// Image attachments fit the new width before the text is laid out again.
|
||||
[_textView _setTextAttachmentsMaximumWidth:MAX(0.0, _size.width - 2.0 * _lineFragmentPadding)];
|
||||
|
||||
_inResizing = YES;
|
||||
[_layoutManager invalidateLayoutForCharacterRange:CPMakeRange(0, [[_layoutManager textStorage] length])
|
||||
isSoft:NO
|
||||
@@ -193,13 +197,16 @@ CPLineMovesUp = 4;
|
||||
|
||||
- (void)textViewFrameChanged:(CPNotification)aNotification
|
||||
{
|
||||
var newSize = CGSizeMake(_size.width, _size.height);
|
||||
// As in Cocoa, a tracking container fills the text view inside its text container inset on both sides.
|
||||
var newSize = CGSizeMake(_size.width, _size.height),
|
||||
frameSize = [_textView frame].size,
|
||||
inset = [_textView textContainerInset];
|
||||
|
||||
if (_widthTracksTextView)
|
||||
newSize.width = [_textView frame].size.width;
|
||||
newSize.width = MAX(0.0, frameSize.width - 2.0 * inset.width);
|
||||
|
||||
if (_heightTracksTextView)
|
||||
newSize.height = [_textView frame].size.height;
|
||||
newSize.height = MAX(0.0, frameSize.height - 2.0 * inset.height);
|
||||
|
||||
[self setContainerSize:newSize];
|
||||
}
|
||||
@@ -271,7 +278,8 @@ CPLineMovesUp = 4;
|
||||
var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
|
||||
CPTextContainerLayoutManagerKey = @"CPTextContainerLayoutManagerKey",
|
||||
CPTextContainerWidthTracksTextViewKey = @"CPTextContainerWidthTracksTextViewKey",
|
||||
CPTextContainerHeightTracksTextViewKey = @"CPTextContainerHeightTracksTextViewKey";
|
||||
CPTextContainerHeightTracksTextViewKey = @"CPTextContainerHeightTracksTextViewKey",
|
||||
CPTextContainerLineFragmentPaddingKey = @"CPTextContainerLineFragmentPaddingKey";
|
||||
|
||||
@implementation CPTextContainer (CPCoding)
|
||||
|
||||
@@ -290,6 +298,10 @@ var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
|
||||
|
||||
_widthTracksTextView = [aCoder decodeBoolForKey:CPTextContainerWidthTracksTextViewKey];
|
||||
_heightTracksTextView = [aCoder decodeBoolForKey:CPTextContainerHeightTracksTextViewKey];
|
||||
|
||||
// Archives made before the padding was archived keep the default from _init.
|
||||
if ([aCoder containsValueForKey:CPTextContainerLineFragmentPaddingKey])
|
||||
_lineFragmentPadding = [aCoder decodeFloatForKey:CPTextContainerLineFragmentPaddingKey];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -301,6 +313,7 @@ var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
|
||||
[aCoder encodeObject:_layoutManager forKey:CPTextContainerLayoutManagerKey];
|
||||
[aCoder encodeBool:_widthTracksTextView forKey:CPTextContainerWidthTracksTextViewKey];
|
||||
[aCoder encodeBool:_heightTracksTextView forKey:CPTextContainerHeightTracksTextViewKey];
|
||||
[aCoder encodeFloat:_lineFragmentPadding forKey:CPTextContainerLineFragmentPaddingKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
* CPTextList.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 24/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* 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/CPObject.j>
|
||||
@import <Foundation/CPAttributedString.j>
|
||||
|
||||
@import "CPParagraphStyle.j"
|
||||
|
||||
/*
|
||||
CPTextListOptions
|
||||
*/
|
||||
CPTextListPrependEnclosingMarker = 1 << 0;
|
||||
|
||||
/*
|
||||
CPTextListMarkerFormat
|
||||
*/
|
||||
CPTextListMarkerBox = @"{box}";
|
||||
CPTextListMarkerCheck = @"{check}";
|
||||
CPTextListMarkerCircle = @"{circle}";
|
||||
CPTextListMarkerDiamond = @"{diamond}";
|
||||
CPTextListMarkerDisc = @"{disc}";
|
||||
CPTextListMarkerHyphen = @"{hyphen}";
|
||||
CPTextListMarkerSquare = @"{square}";
|
||||
CPTextListMarkerLowercaseHexadecimal = @"{lower-hexadecimal}";
|
||||
CPTextListMarkerUppercaseHexadecimal = @"{upper-hexadecimal}";
|
||||
CPTextListMarkerOctal = @"{octal}";
|
||||
CPTextListMarkerLowercaseAlpha = @"{lower-alpha}";
|
||||
CPTextListMarkerUppercaseAlpha = @"{upper-alpha}";
|
||||
CPTextListMarkerLowercaseLatin = @"{lower-latin}";
|
||||
CPTextListMarkerUppercaseLatin = @"{upper-latin}";
|
||||
CPTextListMarkerLowercaseRoman = @"{lower-roman}";
|
||||
CPTextListMarkerUppercaseRoman = @"{upper-roman}";
|
||||
CPTextListMarkerDecimal = @"{decimal}";
|
||||
|
||||
/*
|
||||
The characters that the unordered marker formats stand for.
|
||||
*/
|
||||
var CPTextListBulletCharacters = {
|
||||
"box": "□",
|
||||
"check": "✓",
|
||||
"circle": "◦",
|
||||
"diamond": "◆",
|
||||
"disc": "•",
|
||||
"hyphen": "⁃",
|
||||
"square": "▪"
|
||||
},
|
||||
CPTextListPlaceholderPattern = /\{([a-z-]+)\}/;
|
||||
|
||||
/*
|
||||
Returns anItemNumber as a letter sequence: 1 is "a", 26 is "z", 27 is "aa".
|
||||
*/
|
||||
var _CPTextListAlphaString = function(anItemNumber)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
while (anItemNumber > 0)
|
||||
{
|
||||
anItemNumber--;
|
||||
result = String.fromCharCode(97 + anItemNumber % 26) + result;
|
||||
anItemNumber = Math.floor(anItemNumber / 26);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
Returns anItemNumber as a lowercase Roman numeral.
|
||||
*/
|
||||
var _CPTextListRomanString = function(anItemNumber)
|
||||
{
|
||||
var values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
|
||||
numerals = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"],
|
||||
result = "";
|
||||
|
||||
for (var i = 0; i < values.length; i++)
|
||||
{
|
||||
while (anItemNumber >= values[i])
|
||||
{
|
||||
result += numerals[i];
|
||||
anItemNumber -= values[i];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@class CPTextList
|
||||
|
||||
A CPTextList describes one list in rich text: the format of its markers, its options and the
|
||||
number of its first item. A paragraph belongs to the lists in its paragraph style's
|
||||
\c textLists, outermost first. As in Cocoa, the markers are ordinary text at the start of
|
||||
each item: a tab, the marker and another tab, placed by the paragraph style's indents and
|
||||
tab stops. A list is identified by the object itself, so the paragraphs of one list share
|
||||
the same CPTextList.
|
||||
*/
|
||||
@implementation CPTextList : CPObject
|
||||
{
|
||||
CPString _markerFormat;
|
||||
unsigned _listOptions;
|
||||
int _startingItemNumber;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initialises a list.
|
||||
@param aFormat the marker format: one of the CPTextListMarker constants, optionally with
|
||||
text around it, such as \c @"({decimal})"
|
||||
@param aMask the list options, such as CPTextListPrependEnclosingMarker
|
||||
*/
|
||||
- (id)initWithMarkerFormat:(CPString)aFormat options:(unsigned)aMask
|
||||
{
|
||||
return [self initWithMarkerFormat:aFormat options:aMask startingItemNumber:1];
|
||||
}
|
||||
|
||||
/*!
|
||||
Initialises a list whose first item has the number anItemNumber.
|
||||
*/
|
||||
- (id)initWithMarkerFormat:(CPString)aFormat options:(unsigned)aMask startingItemNumber:(int)anItemNumber
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_markerFormat = aFormat || CPTextListMarkerDisc;
|
||||
_listOptions = aMask;
|
||||
_startingItemNumber = anItemNumber;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the marker format.
|
||||
*/
|
||||
- (CPString)markerFormat
|
||||
{
|
||||
return _markerFormat;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the list options.
|
||||
*/
|
||||
- (unsigned)listOptions
|
||||
{
|
||||
return _listOptions;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the number of the list's first item. The default is 1.
|
||||
*/
|
||||
- (int)startingItemNumber
|
||||
{
|
||||
return _startingItemNumber;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the number of the list's first item.
|
||||
*/
|
||||
- (void)setStartingItemNumber:(int)anItemNumber
|
||||
{
|
||||
_startingItemNumber = anItemNumber;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns \c YES if the markers are numbers or letters, and \c NO if they are bullets.
|
||||
*/
|
||||
- (BOOL)isOrdered
|
||||
{
|
||||
var match = _markerFormat.match(CPTextListPlaceholderPattern);
|
||||
|
||||
return !!match && !CPTextListBulletCharacters.hasOwnProperty(match[1]);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the marker for the item with the number anItemNumber, without the tabs around it.
|
||||
The marker format's placeholder is replaced by the bullet or the formatted number, and the
|
||||
text around the placeholder is kept.
|
||||
*/
|
||||
- (CPString)markerForItemNumber:(int)anItemNumber
|
||||
{
|
||||
var match = _markerFormat.match(CPTextListPlaceholderPattern);
|
||||
|
||||
if (!match)
|
||||
return _markerFormat;
|
||||
|
||||
var name = match[1],
|
||||
marker;
|
||||
|
||||
if (CPTextListBulletCharacters.hasOwnProperty(name))
|
||||
marker = CPTextListBulletCharacters[name];
|
||||
else
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case "lower-hexadecimal":
|
||||
marker = anItemNumber.toString(16);
|
||||
break;
|
||||
|
||||
case "upper-hexadecimal":
|
||||
marker = anItemNumber.toString(16).toUpperCase();
|
||||
break;
|
||||
|
||||
case "octal":
|
||||
marker = anItemNumber.toString(8);
|
||||
break;
|
||||
|
||||
case "lower-alpha":
|
||||
case "lower-latin":
|
||||
marker = anItemNumber > 0 ? _CPTextListAlphaString(anItemNumber) : String(anItemNumber);
|
||||
break;
|
||||
|
||||
case "upper-alpha":
|
||||
case "upper-latin":
|
||||
marker = anItemNumber > 0 ? _CPTextListAlphaString(anItemNumber).toUpperCase() : String(anItemNumber);
|
||||
break;
|
||||
|
||||
case "lower-roman":
|
||||
marker = anItemNumber > 0 ? _CPTextListRomanString(anItemNumber) : String(anItemNumber);
|
||||
break;
|
||||
|
||||
case "upper-roman":
|
||||
marker = anItemNumber > 0 ? _CPTextListRomanString(anItemNumber).toUpperCase() : String(anItemNumber);
|
||||
break;
|
||||
|
||||
default:
|
||||
marker = String(anItemNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return _markerFormat.replace(CPTextListPlaceholderPattern, marker);
|
||||
}
|
||||
|
||||
- (CPString)description
|
||||
{
|
||||
return [super description] + " markerFormat=" + _markerFormat + " startingItemNumber=" + _startingItemNumber;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPTextListMarkerFormatKey = @"CPTextListMarkerFormat",
|
||||
CPTextListOptionsKey = @"CPTextListOptions",
|
||||
CPTextListStartingItemNumberKey = @"CPTextListStartingItemNumber";
|
||||
|
||||
@implementation CPTextList (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_markerFormat = [aCoder decodeObjectForKey:CPTextListMarkerFormatKey] || CPTextListMarkerDisc;
|
||||
_listOptions = [aCoder decodeIntForKey:CPTextListOptionsKey];
|
||||
_startingItemNumber = [aCoder containsValueForKey:CPTextListStartingItemNumberKey] ? [aCoder decodeIntForKey:CPTextListStartingItemNumberKey] : 1;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeObject:_markerFormat forKey:CPTextListMarkerFormatKey];
|
||||
[aCoder encodeInt:_listOptions forKey:CPTextListOptionsKey];
|
||||
[aCoder encodeInt:_startingItemNumber forKey:CPTextListStartingItemNumberKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
Returns YES if the character code separates paragraphs.
|
||||
*/
|
||||
var _CPTextListIsParagraphSeparator = function(aCharCode)
|
||||
{
|
||||
return aCharCode === 10 || aCharCode === 13 || aCharCode === 0x2029;
|
||||
};
|
||||
|
||||
@implementation CPAttributedString (CPTextListAdditions)
|
||||
|
||||
/* @ignore
|
||||
Returns the range of the paragraph containing anIndex, including its paragraph separator.
|
||||
*/
|
||||
- (CPRange)_paragraphRangeForIndex:(unsigned)anIndex
|
||||
{
|
||||
var string = [self string],
|
||||
length = string.length,
|
||||
start = MIN(anIndex, length),
|
||||
end = start;
|
||||
|
||||
while (start > 0 && !_CPTextListIsParagraphSeparator(string.charCodeAt(start - 1)))
|
||||
start--;
|
||||
|
||||
while (end < length && !_CPTextListIsParagraphSeparator(string.charCodeAt(end)))
|
||||
end++;
|
||||
|
||||
if (end < length)
|
||||
end++;
|
||||
|
||||
return CPMakeRange(start, end - start);
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the text lists of the paragraph that starts at aParagraphStart, outermost first.
|
||||
*/
|
||||
- (CPArray)_textListsOfParagraphAtIndex:(unsigned)aParagraphStart
|
||||
{
|
||||
if (aParagraphStart >= [self length])
|
||||
return [];
|
||||
|
||||
var paragraphStyle = [self attribute:CPParagraphStyleAttributeName atIndex:aParagraphStart effectiveRange:nil];
|
||||
|
||||
return [paragraphStyle textLists] || [];
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the range of the paragraphs that belong to aList, around anIndex. The paragraphs
|
||||
are contiguous: the range ends at the first paragraph on either side that does not belong
|
||||
to the list, at any level. Returns a range with the location CPNotFound if the paragraph
|
||||
at anIndex does not belong to aList.
|
||||
*/
|
||||
- (CPRange)rangeOfTextList:(CPTextList)aList atIndex:(unsigned)anIndex
|
||||
{
|
||||
var paragraphRange = [self _paragraphRangeForIndex:anIndex];
|
||||
|
||||
if (![[self _textListsOfParagraphAtIndex:paragraphRange.location] containsObjectIdenticalTo:aList])
|
||||
return CPMakeRange(CPNotFound, 0);
|
||||
|
||||
var start = paragraphRange.location,
|
||||
end = CPMaxRange(paragraphRange),
|
||||
length = [self length];
|
||||
|
||||
while (start > 0)
|
||||
{
|
||||
var previousRange = [self _paragraphRangeForIndex:start - 1];
|
||||
|
||||
if (![[self _textListsOfParagraphAtIndex:previousRange.location] containsObjectIdenticalTo:aList])
|
||||
break;
|
||||
|
||||
start = previousRange.location;
|
||||
}
|
||||
|
||||
while (end < length)
|
||||
{
|
||||
var nextRange = [self _paragraphRangeForIndex:end];
|
||||
|
||||
if (![[self _textListsOfParagraphAtIndex:nextRange.location] containsObjectIdenticalTo:aList])
|
||||
break;
|
||||
|
||||
end = CPMaxRange(nextRange);
|
||||
}
|
||||
|
||||
return CPMakeRange(start, end - start);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the number of the item of aList at anIndex. The items of a list are the paragraphs
|
||||
whose innermost list it is; paragraphs of lists nested in it do not count. For a paragraph
|
||||
of a nested list, the result is the number of the enclosing item. Returns 0 if the
|
||||
paragraph at anIndex does not belong to aList.
|
||||
*/
|
||||
- (int)itemNumberInTextList:(CPTextList)aList atIndex:(unsigned)anIndex
|
||||
{
|
||||
var listRange = [self rangeOfTextList:aList atIndex:anIndex];
|
||||
|
||||
if (listRange.location === CPNotFound)
|
||||
return 0;
|
||||
|
||||
var target = [self _paragraphRangeForIndex:anIndex].location,
|
||||
itemNumber = [aList startingItemNumber] - 1,
|
||||
location = listRange.location;
|
||||
|
||||
while (location <= target)
|
||||
{
|
||||
var paragraphRange = [self _paragraphRangeForIndex:location];
|
||||
|
||||
if ([[self _textListsOfParagraphAtIndex:location] lastObject] === aList)
|
||||
itemNumber++;
|
||||
|
||||
if (!paragraphRange.length)
|
||||
break;
|
||||
|
||||
location = CPMaxRange(paragraphRange);
|
||||
}
|
||||
|
||||
return MAX(itemNumber, [aList startingItemNumber]);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
@import "CPText.j"
|
||||
@import "CPFont.j"
|
||||
@import "CPTextAttachment.j"
|
||||
|
||||
@class CPLayoutManager;
|
||||
|
||||
@@ -263,16 +264,23 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns an instance of CPTextStorage that contains the provided instance of CPView.
|
||||
This can be used to insert arbitrary views into the text. These views are treated as individual characters during editing.
|
||||
This works only with views that conform to the CPCoding protocol
|
||||
Returns an instance of CPTextStorage that contains one attachment character for anAttachment.
|
||||
The attachment is treated as an individual character during editing.
|
||||
|
||||
anAttachment is either a CPTextAttachment, as in Cocoa, or a view. A CPTextAttachment is
|
||||
stored as the character's CPAttachmentAttributeName attribute, and its view is laid out as
|
||||
the character. A view is inserted into the text directly; this works only with views that
|
||||
conform to the CPCoding protocol.
|
||||
*/
|
||||
|
||||
+ (id)attributedStringWithAttachment:(CPView)someView
|
||||
+ (id)attributedStringWithAttachment:(id)anAttachment
|
||||
{
|
||||
var result = [[self alloc] initWithString:_CPAttachmentCharacterAsString];
|
||||
|
||||
[result setAttributes:@{_CPAttachmentView:someView} range:CPMakeRange(0, 1)];
|
||||
if ([anAttachment isKindOfClass:[CPTextAttachment class]])
|
||||
[result setAttributes:@{CPAttachmentAttributeName:anAttachment, _CPAttachmentView:[anAttachment _attachmentView]} range:CPMakeRange(0, 1)];
|
||||
else
|
||||
[result setAttributes:@{_CPAttachmentView:anAttachment} range:CPMakeRange(0, 1)];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+2145
-401
File diff suppressed because it is too large.
Load diff
@@ -49,7 +49,23 @@ CPTypesetterParagraphBreakAction = 1 << 4;
|
||||
CPTypesetterContainerBreakAction = 1 << 5;
|
||||
|
||||
var CPSystemTypesetterFactory,
|
||||
_sharedSimpleTypesetter;
|
||||
_sharedSimpleTypesetter,
|
||||
_CPTypesetterCachedDecimalSeparator = nil;
|
||||
|
||||
/*
|
||||
Returns the decimal separator of the user's locale, which a decimal tab stop aligns on.
|
||||
*/
|
||||
var _CPTypesetterDecimalSeparator = function()
|
||||
{
|
||||
if (!_CPTypesetterCachedDecimalSeparator)
|
||||
{
|
||||
var formatted = (1.5).toLocaleString();
|
||||
|
||||
_CPTypesetterCachedDecimalSeparator = formatted.length === 3 ? formatted.charAt(1) : @".";
|
||||
}
|
||||
|
||||
return _CPTypesetterCachedDecimalSeparator;
|
||||
};
|
||||
|
||||
@implementation CPTypesetter : CPObject
|
||||
|
||||
@@ -170,10 +186,32 @@ var CPSystemTypesetterFactory,
|
||||
advancements:(CPArray)advancements
|
||||
lineCount:(unsigned)lineCount
|
||||
sameLine:(BOOL)sameLine
|
||||
wordWrapped:(BOOL)wordWrapped
|
||||
rightMargin:(float)rightMargin
|
||||
{
|
||||
var myX = 0,
|
||||
rect = CGRectMake(lineOrigin.x, lineOrigin.y, _lineWidth, _lineHeight),
|
||||
containerSize = aContainer._size;
|
||||
// Line positions are computed inside the line fragment padding. The padding is added here, where
|
||||
// the line fragment rectangle is stored, and alignment uses the width inside the padding.
|
||||
var padding = [aContainer lineFragmentPadding],
|
||||
myX = 0,
|
||||
rect = CGRectMake(lineOrigin.x + padding, lineOrigin.y, _lineWidth, _lineHeight);
|
||||
|
||||
// As in Cocoa, text is aligned between the indents, and whitespace at the end of the line hangs
|
||||
// outside the alignment: it does not count in the line's visible width.
|
||||
var theString = [_textStorage string],
|
||||
availableWidth = rightMargin - lineOrigin.x,
|
||||
visibleLength = lineRange.length,
|
||||
visibleWidth = _lineWidth;
|
||||
|
||||
while (visibleLength > 0)
|
||||
{
|
||||
var trailingCharCode = theString.charCodeAt(lineRange.location + visibleLength - 1);
|
||||
|
||||
if (trailingCharCode !== 32 && trailingCharCode !== 10 && trailingCharCode !== 13)
|
||||
break;
|
||||
|
||||
visibleLength--;
|
||||
visibleWidth -= advancements[visibleLength].width;
|
||||
}
|
||||
|
||||
[_layoutManager _appendNewLineFragmentInTextContainer:_currentTextContainer forGlyphRange:lineRange];
|
||||
|
||||
@@ -190,11 +228,41 @@ var CPSystemTypesetterFactory,
|
||||
break;
|
||||
|
||||
case CPCenterTextAlignment:
|
||||
myX = (containerSize.width - _lineWidth) / 2;
|
||||
myX = (availableWidth - visibleWidth) / 2;
|
||||
break;
|
||||
|
||||
case CPRightTextAlignment:
|
||||
myX = containerSize.width - _lineWidth;
|
||||
myX = availableWidth - visibleWidth;
|
||||
break;
|
||||
|
||||
case CPJustifiedTextAlignment:
|
||||
// Only a line that wraps is justified. The last line of a paragraph, and the text
|
||||
// before a tab on the same line, stay left aligned, as in Cocoa. The spare width is
|
||||
// spread evenly over the spaces inside the visible part of the line. The line fragment
|
||||
// draws its spans with the same extra word spacing.
|
||||
if (wordWrapped && !sameLine)
|
||||
{
|
||||
var spaceIndexes = [];
|
||||
|
||||
for (var i = 0; i < visibleLength; i++)
|
||||
{
|
||||
var charCode = theString.charCodeAt(lineRange.location + i);
|
||||
|
||||
if (charCode === 32 || charCode === 160)
|
||||
spaceIndexes.push(i);
|
||||
}
|
||||
|
||||
var spaceCount = spaceIndexes.length,
|
||||
extraWidth = spaceCount ? (availableWidth - visibleWidth) / spaceCount : 0.0;
|
||||
|
||||
if (extraWidth > 0.0)
|
||||
{
|
||||
for (var i = 0; i < spaceCount; i++)
|
||||
advancements[spaceIndexes[i]].width += extraWidth;
|
||||
|
||||
fragment._wordSpacing = extraWidth;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -237,8 +305,10 @@ var CPSystemTypesetterFactory,
|
||||
_lineBase = 0;
|
||||
_lineWidth = 0;
|
||||
|
||||
// containerSizeWidth is the width inside the line fragment padding at both ends.
|
||||
var containerSize = [_currentTextContainer containerSize],
|
||||
containerSizeWidth = containerSize.width,
|
||||
lineFragmentPadding = [_currentTextContainer lineFragmentPadding],
|
||||
containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding,
|
||||
containerSizeHeight = containerSize.height,
|
||||
lineRange = CPMakeRange(glyphIndex, 0),
|
||||
wrapRange = CPMakeRange(0, 0),
|
||||
@@ -274,7 +344,11 @@ var CPSystemTypesetterFactory,
|
||||
rightMargin = containerSizeWidth;
|
||||
|
||||
if (glyphIndex > 0)
|
||||
{
|
||||
// Stored line fragment rectangles include the padding, which the layout below does not.
|
||||
lineOrigin = CGPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin);
|
||||
lineOrigin.x -= lineFragmentPadding;
|
||||
}
|
||||
else if ([_layoutManager extraLineFragmentTextContainer])
|
||||
lineOrigin = CGPointMake(0, [_layoutManager extraLineFragmentUsedRect].origin.y);
|
||||
else
|
||||
@@ -469,7 +543,16 @@ var CPSystemTypesetterFactory,
|
||||
tabAlignment = [nextTab alignment];
|
||||
|
||||
// Mathematically offset the tab character's right boundary
|
||||
if (tabAlignment === CPCenterTextAlignment)
|
||||
if ([nextTab tabStopType] === CPDecimalTabStopType)
|
||||
{
|
||||
// As in Cocoa, the first decimal separator of the user's locale starts at the stop.
|
||||
// A segment without one ends at the stop, like a right tab.
|
||||
var separatorIndex = segmentString.indexOf(_CPTypesetterDecimalSeparator()),
|
||||
widthBeforeSeparator = separatorIndex === -1 ? nextSegmentWidth : [segmentString.substring(0, separatorIndex) sizeWithFont:currentFont inWidth:NULL].width;
|
||||
|
||||
rangeWidth = (tabLocation - widthBeforeSeparator) - lineOrigin.x;
|
||||
}
|
||||
else if (tabAlignment === CPCenterTextAlignment)
|
||||
{
|
||||
rangeWidth = (tabLocation - nextSegmentWidth / 2.0) - lineOrigin.x;
|
||||
}
|
||||
@@ -533,7 +616,7 @@ var CPSystemTypesetterFactory,
|
||||
|
||||
if (isNewline || isTabStop || isAttachment)
|
||||
{
|
||||
if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline])
|
||||
if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline wordWrapped:isWordWrapped rightMargin:rightMargin])
|
||||
return;
|
||||
|
||||
if (isTabStop || isAttachment)
|
||||
@@ -556,7 +639,8 @@ var CPSystemTypesetterFactory,
|
||||
{
|
||||
_currentTextContainer = textContainers[++_indexOfCurrentContainer];
|
||||
containerSize = [_currentTextContainer containerSize];
|
||||
containerSizeWidth = containerSize.width;
|
||||
lineFragmentPadding = [_currentTextContainer lineFragmentPadding];
|
||||
containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding;
|
||||
containerSizeHeight = containerSize.height;
|
||||
}
|
||||
|
||||
@@ -587,9 +671,10 @@ var CPSystemTypesetterFactory,
|
||||
|
||||
// Flush remaining characters
|
||||
if (lineRange.length)
|
||||
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO];
|
||||
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO wordWrapped:NO rightMargin:rightMargin];
|
||||
|
||||
var rect = CGRectMake(1, lineOrigin.y - descent, containerSizeWidth, currentFontLineHeight);
|
||||
// The empty line after the text starts inside the padding, and at least 1 point in, so the insertion point stays visible.
|
||||
var rect = CGRectMake(MAX(1.0, lineFragmentPadding), lineOrigin.y - descent, containerSizeWidth, currentFontLineHeight);
|
||||
[_layoutManager setExtraLineFragmentRect:rect usedRect:rect textContainer:_currentTextContainer];
|
||||
|
||||
var fragment = [_layoutManager._lineFragments lastObject];
|
||||
|
||||
Reference in new issue
Block a user