mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-25 17:45:36 +00:00
AppKit lacked CPTextList and the textLists property on CPParagraphStyle,
preventing rich text from supporting bulleted or numbered lists. Additionally,
CPTextView derived its typing attributes from the character following the
insertion point upon selection changes, causing text appended at the end of a
document (and the Font Panel) to incorrectly revert to default styling.
* AppKit/CPTextView/CPTextList.j:
- Implement CPTextList following NSTextList specifications, including standard
Cocoa marker formats, CPTextListPrependEnclosingMarker, starting item numbers,
markerForItemNumber:, isOrdered, and keyed archiving.
- Add rangeOfTextList:atIndex: and itemNumberInTextList:atIndex: category
extensions to CPAttributedString.
* AppKit/CPTextView/CPParagraphStyle.j:
- Add textLists and setTextLists: properties with copy and archiving support.
- Implement paragraph style equality checks based on list identity.
* AppKit/CPTextView/CPTextView.j:
- Implement setTextList: and the changeTextList: action method (routing marker
styles via sender tags). Adjacent paragraphs with matching styles join automatically.
- Implement list item keyboard navigation and editing mechanics: Return generates
subsequent list items, Return on an empty item terminates list context,
Tab at item origin nests levels, Shift-Tab promotes levels, and Delete at item
text origin strips list membership.
- Automate list item renumbering during live edits, paste operations, and undo/redo cycles.
- Update Paste Ruler routines to correctly synthesize, strip, and merge list markers.
- Fix typingAttributes and Font Panel attribute resolution to derive from the
first selected character or the character immediately preceding the insertion point.
* AppKit/CPKeyBinding.j:
- Bind Shift-Tab selector to insertBacktab:.
* AppKit/CPWindow/CPWindow.j:
- Update key event routing to dispatch Shift-Tab to first responders that accept Tab.
* AppKit/AppKit.j:
- Export CPTextList.
* Tests/AppKit/CPTextListTest.j:
- Add unit test coverage for marker formatting, archiving, list range queries,
interactive list editing, Paste Ruler execution, and menu action validation.
* Tests/AppKit/CPTextViewTest.j:
- Add test coverage verifying typing attributes at trailing insertion points.
* Tests/Manual/TextEditor/Resources/MainMenu.xib, MainMenu.cib:
- Add Format > List menu structures containing bulleted, numbered, and outline choices.
* Tests/Manual/TextEditor/WORKLIST.md:
- Mark list interaction testing as completed; log RTF list serialization as an open item.
CPTextView now provides Cocoa-compliant bulleted, numbered, and nested list support.
422 lines
14 KiB
Plaintext
422 lines
14 KiB
Plaintext
/*
|
|
* CPParagraphStyle.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 <Foundation/CPArray.j>
|
|
@import <Foundation/CPDictionary.j>
|
|
|
|
CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName";
|
|
|
|
@typedef CPTabStopType
|
|
CPLeftTabStopType = 0;
|
|
CPRightTabStopType = 1;
|
|
CPCenterTabStopType = 2;
|
|
CPDecimalTabStopType = 3;
|
|
|
|
// Standard Tab Interval (28pts is roughly 4 spaces in standard fonts)
|
|
var kDefaultTabInterval = 28.0;
|
|
|
|
// MARK: - CPTextTab Implementation
|
|
|
|
@implementation CPTextTab : CPObject
|
|
{
|
|
CPTextAlignment _alignment @accessors(readonly, property=alignment);
|
|
float _location @accessors(readonly, property=location);
|
|
CPDictionary _options @accessors(readonly, property=options);
|
|
}
|
|
|
|
- (id)initWithTextAlignment:(CPTextAlignment)anAlignment location:(float)aLocation options:(CPDictionary)options
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
_alignment = anAlignment;
|
|
_location = aLocation;
|
|
_options = [options copy];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
// Convenience initializer matching AppKit behavior
|
|
- (id)initWithType:(CPTabStopType)aType location:(float)aLocation
|
|
{
|
|
// Map old TabStopType to TextAlignment for modern compatibility
|
|
return [self initWithTextAlignment:aType location:aLocation options:nil];
|
|
}
|
|
|
|
// Added to resolve the unrecognized selector exception in the RTF producer
|
|
- (CPTabStopType)tabStopType
|
|
{
|
|
return _alignment;
|
|
}
|
|
|
|
- (BOOL)isEqual:(id)other
|
|
{
|
|
if (self === other) return YES;
|
|
if (![other isKindOfClass:[CPTextTab class]]) return NO;
|
|
|
|
return _location === [other location] &&
|
|
_alignment === [other alignment] &&
|
|
((_options == nil && [other options] == nil) || [_options isEqualToDictionary:[other options]]);
|
|
}
|
|
|
|
- (id)copy
|
|
{
|
|
return [[CPTextTab alloc] initWithTextAlignment:_alignment location:_location options:_options];
|
|
}
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
_alignment = [aCoder decodeIntForKey:@"CPTextTabAlignment"];
|
|
_location = [aCoder decodeFloatForKey:@"CPTextTabLocation"];
|
|
_options = [aCoder decodeObjectForKey:@"CPTextTabOptions"];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
[aCoder encodeInt:_alignment forKey:@"CPTextTabAlignment"];
|
|
[aCoder encodeFloat:_location forKey:@"CPTextTabLocation"];
|
|
[aCoder encodeObject:_options forKey:@"CPTextTabOptions"];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
// 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
|
|
{
|
|
float _lineSpacing @accessors(readonly, property=lineSpacing);
|
|
float _paragraphSpacing @accessors(readonly, property=paragraphSpacing);
|
|
CPTextAlignment _alignment @accessors(readonly, property=alignment);
|
|
float _headIndent @accessors(readonly, property=headIndent);
|
|
float _tailIndent @accessors(readonly, property=tailIndent);
|
|
float _firstLineHeadIndent @accessors(readonly, property=firstLineHeadIndent);
|
|
float _minimumLineHeight @accessors(readonly, property=minimumLineHeight);
|
|
float _maximumLineHeight @accessors(readonly, property=maximumLineHeight);
|
|
CPLineBreakMode _lineBreakMode @accessors(readonly, property=lineBreakMode);
|
|
CPWritingDirection _baseWritingDirection @accessors(readonly, property=baseWritingDirection);
|
|
float _lineHeightMultiple @accessors(readonly, property=lineHeightMultiple);
|
|
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
|
|
{
|
|
if (!_sharedDefaultParagraphStyle)
|
|
{
|
|
_sharedDefaultParagraphStyle = [[CPParagraphStyle alloc] init];
|
|
// Ensure defaults are set on the shared instance internal vars
|
|
// Since it's immutable, we rely on the init to set these.
|
|
}
|
|
return _sharedDefaultParagraphStyle;
|
|
}
|
|
|
|
+ (CPWritingDirection)defaultWritingDirectionForLanguage:(CPString)languageName
|
|
{
|
|
// Simplified: Cappuccino usually assumes LTR unless specified otherwise.
|
|
return CPWritingDirectionLeftToRight;
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
_lineSpacing = 0.0;
|
|
_paragraphSpacing = 0.0;
|
|
_alignment = CPLeftTextAlignment;
|
|
_headIndent = 0.0;
|
|
_tailIndent = 0.0;
|
|
_firstLineHeadIndent = 0.0;
|
|
_minimumLineHeight = 0.0;
|
|
_maximumLineHeight = 0.0;
|
|
_lineBreakMode = CPLineBreakByWordWrapping;
|
|
_baseWritingDirection = CPWritingDirectionNatural;
|
|
_lineHeightMultiple = 0.0;
|
|
_paragraphSpacingBefore = 0.0;
|
|
_defaultTabInterval = kDefaultTabInterval;
|
|
|
|
// Generate default tab stops
|
|
_tabStops = [];
|
|
for (var i = 1; i <= 12; i++)
|
|
{
|
|
[_tabStops addObject:[[CPTextTab alloc] initWithType:CPLeftTextAlignment
|
|
location:i * kDefaultTabInterval]];
|
|
}
|
|
|
|
_textLists = [];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithParagraphStyle:(CPParagraphStyle)other
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
_lineSpacing = [other lineSpacing];
|
|
_paragraphSpacing = [other paragraphSpacing];
|
|
_alignment = [other alignment];
|
|
_headIndent = [other headIndent];
|
|
_tailIndent = [other tailIndent];
|
|
_firstLineHeadIndent = [other firstLineHeadIndent];
|
|
_minimumLineHeight = [other minimumLineHeight];
|
|
_maximumLineHeight = [other maximumLineHeight];
|
|
_lineBreakMode = [other lineBreakMode];
|
|
_baseWritingDirection = [other baseWritingDirection];
|
|
_lineHeightMultiple = [other lineHeightMultiple];
|
|
_paragraphSpacingBefore = [other paragraphSpacingBefore];
|
|
_defaultTabInterval = [other defaultTabInterval];
|
|
_tabStops = [[other tabStops] copy];
|
|
_textLists = [[other textLists] copy] || [];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)copy
|
|
{
|
|
// Since this class is immutable, return self.
|
|
// Subclasses (Mutable) will override.
|
|
if ([self class] === [CPParagraphStyle class])
|
|
return self;
|
|
|
|
return [[CPParagraphStyle alloc] initWithParagraphStyle:self];
|
|
}
|
|
|
|
- (id)mutableCopy
|
|
{
|
|
return [[CPMutableParagraphStyle alloc] initWithParagraphStyle:self];
|
|
}
|
|
|
|
// MARK: - Coding Support
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
if (self = [super init])
|
|
{
|
|
_lineSpacing = [aCoder decodeFloatForKey:@"CPParagraphStyleLineSpacing"];
|
|
_paragraphSpacing = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacing"];
|
|
_alignment = [aCoder decodeIntForKey:@"CPParagraphStyleAlignment"];
|
|
_headIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleHeadIndent"];
|
|
_tailIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleTailIndent"];
|
|
_firstLineHeadIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleFirstLineHeadIndent"];
|
|
_minimumLineHeight = [aCoder decodeFloatForKey:@"CPParagraphStyleMinimumLineHeight"];
|
|
_maximumLineHeight = [aCoder decodeFloatForKey:@"CPParagraphStyleMaximumLineHeight"];
|
|
_lineBreakMode = [aCoder decodeIntForKey:@"CPParagraphStyleLineBreakMode"];
|
|
_baseWritingDirection = [aCoder decodeIntForKey:@"CPParagraphStyleBaseWritingDirection"];
|
|
_lineHeightMultiple = [aCoder decodeFloatForKey:@"CPParagraphStyleLineHeightMultiple"];
|
|
_paragraphSpacingBefore = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacingBefore"];
|
|
_defaultTabInterval = [aCoder decodeFloatForKey:@"CPParagraphStyleDefaultTabInterval"];
|
|
_tabStops = [aCoder decodeObjectForKey:@"CPParagraphStyleTabStops"];
|
|
_textLists = [aCoder decodeObjectForKey:@"CPParagraphStyleTextLists"] || [];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
[aCoder encodeFloat:_lineSpacing forKey:@"CPParagraphStyleLineSpacing"];
|
|
[aCoder encodeFloat:_paragraphSpacing forKey:@"CPParagraphStyleParagraphSpacing"];
|
|
[aCoder encodeInt:_alignment forKey:@"CPParagraphStyleAlignment"];
|
|
[aCoder encodeFloat:_headIndent forKey:@"CPParagraphStyleHeadIndent"];
|
|
[aCoder encodeFloat:_tailIndent forKey:@"CPParagraphStyleTailIndent"];
|
|
[aCoder encodeFloat:_firstLineHeadIndent forKey:@"CPParagraphStyleFirstLineHeadIndent"];
|
|
[aCoder encodeFloat:_minimumLineHeight forKey:@"CPParagraphStyleMinimumLineHeight"];
|
|
[aCoder encodeFloat:_maximumLineHeight forKey:@"CPParagraphStyleMaximumLineHeight"];
|
|
[aCoder encodeInt:_lineBreakMode forKey:@"CPParagraphStyleLineBreakMode"];
|
|
[aCoder encodeInt:_baseWritingDirection forKey:@"CPParagraphStyleBaseWritingDirection"];
|
|
[aCoder encodeFloat:_lineHeightMultiple forKey:@"CPParagraphStyleLineHeightMultiple"];
|
|
[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
|
|
|
|
- (BOOL)isEqual:(id)other
|
|
{
|
|
if (self === other) return YES;
|
|
if (![other isKindOfClass:[CPParagraphStyle class]]) return NO;
|
|
|
|
return _lineSpacing === [other lineSpacing] &&
|
|
_paragraphSpacing === [other paragraphSpacing] &&
|
|
_alignment === [other alignment] &&
|
|
_headIndent === [other headIndent] &&
|
|
_tailIndent === [other tailIndent] &&
|
|
_firstLineHeadIndent === [other firstLineHeadIndent] &&
|
|
_lineBreakMode === [other lineBreakMode] &&
|
|
[_tabStops isEqualToArray:[other tabStops]] &&
|
|
_CPParagraphStyleTextListsAreIdentical(_textLists, [other textLists]);
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
// MARK: - CPMutableParagraphStyle Implementation
|
|
|
|
@implementation CPMutableParagraphStyle : CPParagraphStyle
|
|
{
|
|
}
|
|
|
|
- (id)initWithParagraphStyle:(CPParagraphStyle)other
|
|
{
|
|
if (self = [super initWithParagraphStyle:other])
|
|
{
|
|
// Ensure our tab stops array is mutable in the mutable subclass
|
|
_tabStops = [[other tabStops] mutableCopy];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
if (self = [super initWithCoder:aCoder])
|
|
{
|
|
_tabStops = [_tabStops mutableCopy];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)setLineSpacing:(float)aLineSpacing
|
|
{
|
|
_lineSpacing = aLineSpacing;
|
|
}
|
|
|
|
- (void)setParagraphSpacing:(float)aParagraphSpacing
|
|
{
|
|
_paragraphSpacing = aParagraphSpacing;
|
|
}
|
|
|
|
- (void)setAlignment:(CPTextAlignment)anAlignment
|
|
{
|
|
_alignment = anAlignment;
|
|
}
|
|
|
|
- (void)setHeadIndent:(float)aHeadIndent
|
|
{
|
|
_headIndent = aHeadIndent;
|
|
}
|
|
|
|
- (void)setTailIndent:(float)aTailIndent
|
|
{
|
|
_tailIndent = aTailIndent;
|
|
}
|
|
|
|
- (void)setFirstLineHeadIndent:(float)aFirstLineHeadIndent
|
|
{
|
|
_firstLineHeadIndent = aFirstLineHeadIndent;
|
|
}
|
|
|
|
- (void)setMinimumLineHeight:(float)aMinimumLineHeight
|
|
{
|
|
_minimumLineHeight = aMinimumLineHeight;
|
|
}
|
|
|
|
- (void)setMaximumLineHeight:(float)aMaximumLineHeight
|
|
{
|
|
_maximumLineHeight = aMaximumLineHeight;
|
|
}
|
|
|
|
- (void)setLineBreakMode:(CPLineBreakMode)aLineBreakMode
|
|
{
|
|
_lineBreakMode = aLineBreakMode;
|
|
}
|
|
|
|
- (void)setBaseWritingDirection:(CPWritingDirection)aBaseWritingDirection
|
|
{
|
|
_baseWritingDirection = aBaseWritingDirection;
|
|
}
|
|
|
|
- (void)setLineHeightMultiple:(float)aLineHeightMultiple
|
|
{
|
|
_lineHeightMultiple = aLineHeightMultiple;
|
|
}
|
|
|
|
- (void)setParagraphSpacingBefore:(float)aParagraphSpacingBefore
|
|
{
|
|
_paragraphSpacingBefore = aParagraphSpacingBefore;
|
|
}
|
|
|
|
- (void)setDefaultTabInterval:(float)aDefaultTabInterval
|
|
{
|
|
_defaultTabInterval = aDefaultTabInterval;
|
|
}
|
|
|
|
- (void)addTabStop:(CPTextTab)aTabStop
|
|
{
|
|
[_tabStops addObject:aTabStop];
|
|
}
|
|
|
|
- (void)removeTabStop:(CPTextTab)aTabStop
|
|
{
|
|
[_tabStops removeObject:aTabStop];
|
|
}
|
|
|
|
- (void)setTabStops:(CPArray)newTabStops
|
|
{
|
|
if (_tabStops === newTabStops) return;
|
|
_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
|
|
return [[CPParagraphStyle alloc] initWithParagraphStyle:self];
|
|
}
|
|
|
|
@end
|