mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-25 09:35:36 +00:00
Merge pull request #3342 from enquora/integration-test-document-based-app
Merge TextEditor fixes: text lists * AppKit: Add CPTextList and textLists in CPParagraphStyle, modelled on Cocoa. CPTextView supports bulleted, numbered and nested lists, with Cocoa's editing behaviour for Return, Tab, Shift-Tab and Delete, and numbering that follows each change. Paste Ruler carries lists. * AppKit: Typing attributes and the font panel take the style of the character before the insertion point, as in Cocoa. * AppKit: Shift-Tab reaches a text view that takes the Tab key, and is bound to insertBacktab:. * TextEditor: Format > List applies list styles. The work list records RTF support for lists as open.
This commit is contained in:
11 files changed
+1541
-14
No files matched your search
@@ -102,6 +102,7 @@
|
||||
@import "CPTabView.j"
|
||||
@import "CPText.j"
|
||||
@import "CPTextField.j"
|
||||
@import "CPTextList.j"
|
||||
@import "CPTextView.j"
|
||||
@import "CPTokenField.j"
|
||||
@import "CPToolbar.j"
|
||||
|
||||
@@ -88,6 +88,7 @@ CPStandardKeyBindings[@"~" + CPDeleteFunctionKey] = @"deleteWordForward:";
|
||||
CPStandardKeyBindings[CPTabCharacter] = @"insertTab:";
|
||||
CPStandardKeyBindings[@"~" + CPTabCharacter] = @"insertTabIgnoringFieldEditor:";
|
||||
CPStandardKeyBindings[@"^" + CPTabCharacter] = @"selectNextKeyView:";
|
||||
CPStandardKeyBindings[@"$" + CPTabCharacter] = @"insertBacktab:";
|
||||
CPStandardKeyBindings[CPBackTabCharacter] = @"insertBacktab:";
|
||||
CPStandardKeyBindings[@"^" + CPBackTabCharacter] = @"selectPreviousKeyView:";
|
||||
|
||||
|
||||
@@ -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,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
|
||||
@@ -31,6 +31,7 @@
|
||||
@import "CPTextStorage.j"
|
||||
@import "CPLayoutManager.j"
|
||||
@import "CPParagraphStyle.j"
|
||||
@import "CPTextList.j"
|
||||
@import "CPTextContainer.j"
|
||||
@import "CPRulerView.j"
|
||||
|
||||
@@ -721,7 +722,9 @@ var kDelegateRespondsTo_textShouldBeginEditing
|
||||
For \c -toggleRuler:, the item's title becomes "Hide Ruler" or "Show Ruler" to reflect the
|
||||
ruler's visibility, and the item is enabled only when the text view uses a ruler and lies
|
||||
in a scroll view. \c -copyRuler: is enabled in rich text, and \c -pasteRuler: in editable
|
||||
rich text when the ruler pasteboard holds a ruler. \c -copyFont: is enabled in rich text, and
|
||||
rich text when the ruler pasteboard holds a ruler. \c -changeTextList: is enabled in editable
|
||||
rich text, and its item is checked when it shows the list style at the selection.
|
||||
\c -copyFont: is enabled in rich text, and
|
||||
\c -pasteFont: in editable rich text when the font pasteboard holds a style. The superscript,
|
||||
subscript and baseline actions are enabled in editable rich text. Every other item is enabled.
|
||||
@param anItem the item to validate
|
||||
@@ -749,6 +752,15 @@ var kDelegateRespondsTo_textShouldBeginEditing
|
||||
if (action === @selector(pasteRuler:))
|
||||
return [self isRichText] && [self isEditable] && !![[CPPasteboard pasteboardWithName:CPRulerPboard] availableTypeFromArray:[CPRulerPboardType]];
|
||||
|
||||
// Lists apply only to editable rich text. The item for the list style at the selection is checked.
|
||||
if (action === @selector(changeTextList:))
|
||||
{
|
||||
if ([anItem respondsToSelector:@selector(setState:)])
|
||||
[anItem setState:[self _textListStyleMatchesItem:anItem] ? CPOnState : CPOffState];
|
||||
|
||||
return [self isEditable] && [self isRichText];
|
||||
}
|
||||
|
||||
// As in Cocoa, styles can be copied and pasted only in rich text.
|
||||
if (action === @selector(copyFont:))
|
||||
return [self isRichText];
|
||||
@@ -910,6 +922,7 @@ var kDelegateRespondsTo_textShouldBeginEditing
|
||||
|
||||
[_textStorage replaceCharactersInRange:aRange withAttributedString:aString];
|
||||
[self _fixupReplaceForRange:selectionRange];
|
||||
[self _updateTextListMarkersAroundRange:CPMakeRange(aRange.location, [aString length])];
|
||||
}
|
||||
|
||||
- (void)insertText:(CPString)aString
|
||||
@@ -938,7 +951,10 @@ var kDelegateRespondsTo_textShouldBeginEditing
|
||||
[self didChangeValueForKey:@"objectValue"];
|
||||
[self _continuouslyReverseSetBinding];
|
||||
|
||||
var insertionLocation = _selectionRange.location;
|
||||
|
||||
[self _setSelectedRange:CPMakeRange(_selectionRange.location + [string length], 0) affinity:0 stillSelecting:NO overwriteTypingAttributes:NO];
|
||||
[self _updateTextListMarkersAroundRange:CPMakeRange(insertionLocation, [string length])];
|
||||
_startTrackingLocation = _selectionRange.location;
|
||||
|
||||
[self didChangeText];
|
||||
@@ -1049,6 +1065,33 @@ Sets the selection to a range of characters in response to user action.
|
||||
@param selecting YES to behave appropriately for a continuing selection where the user is still dragging the mouse, NO otherwise. If YES, the receiver doesn’t send notifications or remove the marking from its marked text. If NO, the receiver posts an NSTextViewDidChangeSelectionNotification to the default notification center and removes the marking from marked text if the new selection is greater than the marked region.
|
||||
@param doOverwrite YES to override typing attributes. NO to not override.
|
||||
*/
|
||||
/* @ignore
|
||||
Returns the typing attributes for a selection, as in Cocoa: those of the first character of
|
||||
a selection, or of the character before an insertion point. At the start of a paragraph the
|
||||
insertion point takes the attributes of the paragraph's first character, so that it keeps
|
||||
the paragraph's own style. At the end of the text it takes those of the last character.
|
||||
*/
|
||||
- (CPDictionary)_typingAttributesForSelectionRange:(CPRange)aRange
|
||||
{
|
||||
var string = [_textStorage string],
|
||||
length = string.length,
|
||||
index = aRange.location;
|
||||
|
||||
if (!length)
|
||||
return [_typingAttributes copy];
|
||||
|
||||
if (!aRange.length && index > 0)
|
||||
{
|
||||
var previousCharCode = string.charCodeAt(index - 1),
|
||||
isParagraphStart = previousCharCode === 10 || previousCharCode === 13 || previousCharCode === 0x2029;
|
||||
|
||||
if (!isParagraphStart || index >= length)
|
||||
index--;
|
||||
}
|
||||
|
||||
return [_textStorage attributesAtIndex:MIN(index, length - 1) effectiveRange:nil];
|
||||
}
|
||||
|
||||
- (void)_setSelectedRange:(CPRange)range affinity:(CPSelectionAffinity)affinity stillSelecting:(BOOL)selecting overwriteTypingAttributes:(BOOL)doOverwrite
|
||||
{
|
||||
var maxRange = CPMakeRange(0, [_layoutManager numberOfCharacters]),
|
||||
@@ -1088,7 +1131,7 @@ Sets the selection to a range of characters in response to user action.
|
||||
isNewSelection = !CPEqualRanges(newSelectionRange, _mouseDownOldSelection);
|
||||
|
||||
if (doOverwrite && _placeholderString == nil && isNewSelection)
|
||||
[self setTypingAttributes:[_textStorage attributesAtIndex:CPMaxRange(range) effectiveRange:nil]];
|
||||
[self setTypingAttributes:[self _typingAttributesForSelectionRange:newSelectionRange]];
|
||||
|
||||
// The ruler shows the paragraph style at the selection.
|
||||
if (isNewSelection)
|
||||
@@ -1790,6 +1833,7 @@ Sets the selection to a range of characters in response to user action.
|
||||
[self _continuouslyReverseSetBinding];
|
||||
|
||||
[self setSelectedRange:CPMakeRange(changedRange.location, 0)];
|
||||
[self _updateTextListMarkersAroundRange:CPMakeRange(changedRange.location, 0)];
|
||||
[self didChangeText];
|
||||
[_layoutManager _validateLayoutAndGlyphs];
|
||||
[self sizeToFit];
|
||||
@@ -1823,6 +1867,9 @@ Sets the selection to a range of characters in response to user action.
|
||||
|
||||
- (void)deleteBackward:(id)sender
|
||||
{
|
||||
if ([self _deleteBackwardInTextList])
|
||||
return;
|
||||
|
||||
[self deleteBackward:self handleSmart:[self _shouldUseSmartPasting] && _selectionRange.length > 0];
|
||||
}
|
||||
|
||||
@@ -1861,9 +1908,25 @@ Sets the selection to a range of characters in response to user action.
|
||||
|
||||
- (void)insertTab:(id)sender
|
||||
{
|
||||
if ([self _insertTabInTextList])
|
||||
return;
|
||||
|
||||
[self insertText:@"\t"];
|
||||
}
|
||||
|
||||
/*!
|
||||
Shift-Tab in a list item returns the item to the enclosing list. Elsewhere it moves to the
|
||||
previous key view.
|
||||
@param sender the object requesting this
|
||||
*/
|
||||
- (void)insertBacktab:(id)sender
|
||||
{
|
||||
if ([self _insertBacktabInTextList])
|
||||
return;
|
||||
|
||||
[[self window] selectPreviousKeyView:self];
|
||||
}
|
||||
|
||||
/*
|
||||
@ignore
|
||||
As in Cocoa, an editable text view takes the Tab key and inserts a tab character.
|
||||
@@ -1886,6 +1949,12 @@ Sets the selection to a range of characters in response to user action.
|
||||
|
||||
- (void)insertNewline:(id)sender
|
||||
{
|
||||
if ([self _insertNewlineInTextList])
|
||||
{
|
||||
[[self window] _temporarilyDisableKeyEquivalentForDefaultButton];
|
||||
return;
|
||||
}
|
||||
|
||||
[self insertLineBreak:sender];
|
||||
}
|
||||
|
||||
@@ -1933,7 +2002,9 @@ Sets the selection to a range of characters in response to user action.
|
||||
|
||||
- (CPDictionary)_attributesForFontPanel
|
||||
{
|
||||
var attributes = [[_textStorage attributesAtIndex:CPMaxRange(_selectionRange) effectiveRange:nil] copy];
|
||||
// The font panel shows the font that typing would use: that of the first selected character,
|
||||
// or of the character before the insertion point.
|
||||
var attributes = [[self _typingAttributesForSelectionRange:_selectionRange] mutableCopy];
|
||||
|
||||
[self _enrichEssentialTypingAttributes:attributes];
|
||||
|
||||
@@ -2990,15 +3061,24 @@ var _CPTextViewRulerImageNameForTab = function(aTab)
|
||||
|
||||
/*!
|
||||
Applies the paragraph style on the ruler pasteboard to the selected paragraphs, or to
|
||||
the paragraph at the insertion point.
|
||||
the paragraph at the insertion point. When the style or the paragraphs belong to text
|
||||
lists, the whole paragraphs change, and their list markers are added, removed and
|
||||
numbered as for Format > List.
|
||||
@param aSender the object requesting this
|
||||
*/
|
||||
- (@action)pasteRuler:(id)aSender
|
||||
{
|
||||
var data = [[CPPasteboard pasteboardWithName:CPRulerPboard] dataForType:CPRulerPboardType];
|
||||
|
||||
if (data)
|
||||
[self _applyRulerParagraphStyle:[CPKeyedUnarchiver unarchiveObjectWithData:data]];
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
var paragraphStyle = [CPKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
|
||||
if ([[paragraphStyle textLists] count] || [self _selectionTouchesTextList])
|
||||
[self _pasteRulerParagraphStyleWithTextLists:paragraphStyle];
|
||||
else
|
||||
[self _applyRulerParagraphStyle:paragraphStyle];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -3485,6 +3565,673 @@ var _CPTextViewRulerImageNameForTab = function(aTab)
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
The indent of each list level, and the distance from the start of a level to its markers,
|
||||
in points. As in TextEdit, the first level's markers start 11 points in, and its text starts
|
||||
36 points in.
|
||||
*/
|
||||
var CPTextViewTextListLevelIndent = 36.0,
|
||||
CPTextViewTextListMarkerIndent = 11.0;
|
||||
|
||||
/*
|
||||
The list styles that -changeTextList: applies, by the tag of the sender. Tag 0, or any tag
|
||||
not listed, removes the list.
|
||||
*/
|
||||
var CPTextViewTextListStyles = {
|
||||
1: {format: @"{disc}", options: 0},
|
||||
2: {format: @"{circle}", options: 0},
|
||||
3: {format: @"{square}", options: 0},
|
||||
4: {format: @"{hyphen}", options: 0},
|
||||
10: {format: @"{decimal}.", options: 0},
|
||||
11: {format: @"{lower-alpha}.", options: 0},
|
||||
12: {format: @"{upper-alpha}.", options: 0},
|
||||
13: {format: @"{lower-roman}.", options: 0},
|
||||
14: {format: @"{upper-roman}.", options: 0},
|
||||
20: {format: @"{decimal}.", options: CPTextListPrependEnclosingMarker}
|
||||
};
|
||||
|
||||
/*!
|
||||
Text list support. As in Cocoa, the markers of a list are ordinary text at the start of each
|
||||
item: a tab, the marker and another tab. The paragraph style of an item lists its text lists,
|
||||
outermost first, and sets the indents and tab stops that place the marker and the text.
|
||||
Return in an item starts the next item, and Return in an empty item ends the list. Tab at the
|
||||
start of an item makes it an item of a nested list, and Shift-Tab returns it to the enclosing
|
||||
list. Delete at the start of an item's text removes the item from its list. After every change,
|
||||
the markers of the lists around the change are numbered again.
|
||||
*/
|
||||
@implementation CPTextView (CPTextListSupport)
|
||||
|
||||
// MARK: Changing Lists
|
||||
|
||||
/*!
|
||||
Applies the list style that the sender's tag selects to the selected paragraphs, or to the
|
||||
paragraph at the insertion point. The tags are: 0 for no list; 1 bullet, 2 circle, 3 square
|
||||
and 4 hyphen; 10 for 1. 2. 3., 11 for a. b. c., 12 for A. B. C., 13 for i. ii. iii. and 14
|
||||
for I. II. III.; 20 for 1. with nested items numbered 1.1. A paragraph next to a list of the
|
||||
same style at the same level joins that list, so that its numbering continues.
|
||||
@param aSender the menu item or control that requests the change
|
||||
*/
|
||||
- (@action)changeTextList:(id)aSender
|
||||
{
|
||||
var style = CPTextViewTextListStyles[[aSender tag]];
|
||||
|
||||
[self setTextList:style ? [self _textListWithMarkerFormat:style.format options:style.options] : nil];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the list of the paragraph just before or after the selected paragraphs when it has
|
||||
the marker format and options at the selection's level, or a new list.
|
||||
*/
|
||||
- (CPTextList)_textListWithMarkerFormat:(CPString)aFormat options:(unsigned)anOptions
|
||||
{
|
||||
var paragraphsRange = [self _textListParagraphsRangeForRange:_selectionRange],
|
||||
level = MAX(1, [[_textStorage _textListsOfParagraphAtIndex:paragraphsRange.location] count]),
|
||||
neighbours = [];
|
||||
|
||||
if (paragraphsRange.location > 0)
|
||||
neighbours.push([_textStorage _paragraphRangeForIndex:paragraphsRange.location - 1].location);
|
||||
|
||||
if (CPMaxRange(paragraphsRange) < [_textStorage length])
|
||||
neighbours.push(CPMaxRange(paragraphsRange));
|
||||
|
||||
for (var i = 0; i < neighbours.length; i++)
|
||||
{
|
||||
var lists = [_textStorage _textListsOfParagraphAtIndex:neighbours[i]],
|
||||
list = [lists lastObject];
|
||||
|
||||
if ([lists count] === level && [list markerFormat] === aFormat && [list listOptions] === anOptions)
|
||||
return list;
|
||||
}
|
||||
|
||||
return [[CPTextList alloc] initWithMarkerFormat:aFormat options:anOptions];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns YES if the menu item's tag selects the style of the innermost list at the
|
||||
selection, or no list when the selection is not in a list.
|
||||
*/
|
||||
- (BOOL)_textListStyleMatchesItem:(id)anItem
|
||||
{
|
||||
var paragraphStart = [_textStorage _paragraphRangeForIndex:_selectionRange.location].location,
|
||||
list = [[_textStorage _textListsOfParagraphAtIndex:paragraphStart] lastObject],
|
||||
style = CPTextViewTextListStyles[[anItem tag]];
|
||||
|
||||
if (!list)
|
||||
return !style;
|
||||
|
||||
return !!style && [list markerFormat] === style.format && [list listOptions] === style.options;
|
||||
}
|
||||
|
||||
/*!
|
||||
Makes the selected paragraphs, or the paragraph at the insertion point, items of aList. A
|
||||
paragraph that is already an item of a list changes to aList at the same level. With \c nil,
|
||||
the paragraphs are removed from their lists, at every level.
|
||||
@param aList the list, or \c nil
|
||||
*/
|
||||
- (void)setTextList:(CPTextList)aList
|
||||
{
|
||||
if (![self isRichText])
|
||||
return;
|
||||
|
||||
var paragraphsRange = [self _textListParagraphsRangeForRange:_selectionRange],
|
||||
listsForParagraphs = [],
|
||||
location = paragraphsRange.location;
|
||||
|
||||
do
|
||||
{
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:location],
|
||||
lists = [[_textStorage _textListsOfParagraphAtIndex:paragraphRange.location] copy];
|
||||
|
||||
if (!aList)
|
||||
lists = [];
|
||||
else if ([lists count])
|
||||
lists[[lists count] - 1] = aList;
|
||||
else
|
||||
lists = [aList];
|
||||
|
||||
listsForParagraphs.push(lists);
|
||||
location = CPMaxRange(paragraphRange);
|
||||
}
|
||||
while (location < CPMaxRange(paragraphsRange));
|
||||
|
||||
[self _setTextLists:listsForParagraphs forParagraphsInRange:paragraphsRange];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Makes the item at the insertion point an item of a list nested in its list. The item joins
|
||||
the nested list of the item before it, when there is one, so that its numbering continues.
|
||||
Otherwise a new nested list takes the marker format and options of the enclosing list.
|
||||
*/
|
||||
- (void)_indentTextListItemAtIndex:(unsigned)anIndex
|
||||
{
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:anIndex],
|
||||
lists = [_textStorage _textListsOfParagraphAtIndex:paragraphRange.location],
|
||||
level = [lists count],
|
||||
nestedList = nil;
|
||||
|
||||
if (paragraphRange.location > 0)
|
||||
{
|
||||
var previousRange = [_textStorage _paragraphRangeForIndex:paragraphRange.location - 1],
|
||||
previousLists = [_textStorage _textListsOfParagraphAtIndex:previousRange.location];
|
||||
|
||||
if ([previousLists count] > level && [[previousLists subarrayWithRange:CPMakeRange(0, level)] isEqualToArray:lists])
|
||||
nestedList = previousLists[level];
|
||||
}
|
||||
|
||||
if (!nestedList)
|
||||
{
|
||||
var enclosingList = [lists lastObject];
|
||||
|
||||
nestedList = [[CPTextList alloc] initWithMarkerFormat:[enclosingList markerFormat] options:[enclosingList listOptions]];
|
||||
}
|
||||
|
||||
[self _setTextLists:[[lists arrayByAddingObject:nestedList]] forParagraphsInRange:paragraphRange];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the item at anIndex to the enclosing list, or removes it from its list when the list
|
||||
is not nested.
|
||||
*/
|
||||
- (void)_outdentTextListItemAtIndex:(unsigned)anIndex
|
||||
{
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:anIndex],
|
||||
lists = [_textStorage _textListsOfParagraphAtIndex:paragraphRange.location];
|
||||
|
||||
[self _setTextLists:[[lists subarrayWithRange:CPMakeRange(0, MAX(0, [lists count] - 1))]] forParagraphsInRange:paragraphRange];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Gives each paragraph in aParagraphsRange the text lists at the same position in
|
||||
listsForParagraphs, with a matching paragraph style and marker. The change can be undone.
|
||||
The insertion point or selection keeps its place in the text after the markers.
|
||||
*/
|
||||
- (void)_setTextLists:(CPArray)listsForParagraphs forParagraphsInRange:(CPRange)aParagraphsRange
|
||||
{
|
||||
[self _setTextLists:listsForParagraphs forParagraphsInRange:aParagraphsRange paragraphStyle:nil];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
As -_setTextLists:forParagraphsInRange:, but every paragraph takes a copy of aParagraphStyle,
|
||||
with its text lists, in place of the indents and tab stops that the lists would set. With
|
||||
\c nil, each paragraph keeps its own style and takes the indents and tab stops of its lists.
|
||||
*/
|
||||
- (void)_setTextLists:(CPArray)listsForParagraphs forParagraphsInRange:(CPRange)aParagraphsRange paragraphStyle:(CPParagraphStyle)aParagraphStyle
|
||||
{
|
||||
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:aParagraphsRange replacementString:nil])
|
||||
return;
|
||||
|
||||
var result = [[CPAttributedString alloc] initWithString:@""],
|
||||
selectionStart = _selectionRange.location,
|
||||
selectionEnd = CPMaxRange(_selectionRange),
|
||||
newSelectionStart = selectionStart,
|
||||
newSelectionEnd = selectionEnd,
|
||||
location = aParagraphsRange.location,
|
||||
caretParagraphStyle = nil,
|
||||
index = 0;
|
||||
|
||||
do
|
||||
{
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:location],
|
||||
markerRange = [self _textListMarkerRangeInParagraphRange:paragraphRange],
|
||||
oldMarkerLength = markerRange.location === CPNotFound ? 0 : markerRange.length,
|
||||
paragraph = [[CPAttributedString alloc] initWithAttributedString:[_textStorage attributedSubstringFromRange:paragraphRange]],
|
||||
textStart = paragraphRange.location + oldMarkerLength,
|
||||
lists = listsForParagraphs[index++],
|
||||
attributes = [self _textListAttributesForParagraphRange:paragraphRange textStart:textStart],
|
||||
paragraphStyle = aParagraphStyle ? [aParagraphStyle mutableCopy] : [self _paragraphStyle:[attributes objectForKey:CPParagraphStyleAttributeName] withTextLists:lists],
|
||||
newMarkerLength = [lists count] ? 2 : 0,
|
||||
newParagraphStart = aParagraphsRange.location + [result length];
|
||||
|
||||
if (oldMarkerLength)
|
||||
[paragraph deleteCharactersInRange:CPMakeRange(0, oldMarkerLength)];
|
||||
|
||||
if ([paragraph length])
|
||||
[paragraph addAttribute:CPParagraphStyleAttributeName value:paragraphStyle range:CPMakeRange(0, [paragraph length])];
|
||||
|
||||
// The marker is filled in when the list is numbered, after the change.
|
||||
if (newMarkerLength)
|
||||
{
|
||||
var markerAttributes = [attributes mutableCopy];
|
||||
|
||||
[markerAttributes setObject:paragraphStyle forKey:CPParagraphStyleAttributeName];
|
||||
[paragraph insertAttributedString:[[CPAttributedString alloc] initWithString:@"\t\t" attributes:markerAttributes] atIndex:0];
|
||||
}
|
||||
|
||||
// Positions in the text keep their place after the marker; positions in the old marker
|
||||
// move to the start of the text.
|
||||
if (CPLocationInRange(selectionStart, paragraphRange) || (selectionStart === CPMaxRange(paragraphRange) && selectionStart === CPMaxRange(aParagraphsRange)))
|
||||
{
|
||||
newSelectionStart = newParagraphStart + newMarkerLength + MAX(0, selectionStart - textStart);
|
||||
caretParagraphStyle = paragraphStyle;
|
||||
}
|
||||
|
||||
if (CPLocationInRange(selectionEnd, paragraphRange) || (selectionEnd === CPMaxRange(paragraphRange) && selectionEnd === CPMaxRange(aParagraphsRange)))
|
||||
newSelectionEnd = newParagraphStart + newMarkerLength + MAX(0, selectionEnd - textStart);
|
||||
|
||||
[result appendAttributedString:paragraph];
|
||||
location = CPMaxRange(paragraphRange);
|
||||
}
|
||||
while (location < CPMaxRange(aParagraphsRange));
|
||||
|
||||
[self _replaceCharactersInRange:CPMakeRangeCopy(aParagraphsRange)
|
||||
withAttributedString:result
|
||||
selectionRange:CPMakeRange(newSelectionStart, MAX(0, newSelectionEnd - newSelectionStart))];
|
||||
|
||||
if (caretParagraphStyle)
|
||||
{
|
||||
[_typingAttributes setObject:caretParagraphStyle forKey:CPParagraphStyleAttributeName];
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
|
||||
}
|
||||
|
||||
[self updateRuler];
|
||||
[self didChangeText];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns YES if any paragraph that the selection touches is a list item.
|
||||
*/
|
||||
- (BOOL)_selectionTouchesTextList
|
||||
{
|
||||
var paragraphsRange = [self _textListParagraphsRangeForRange:_selectionRange],
|
||||
location = paragraphsRange.location;
|
||||
|
||||
do
|
||||
{
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:location];
|
||||
|
||||
if ([[_textStorage _textListsOfParagraphAtIndex:paragraphRange.location] count])
|
||||
return YES;
|
||||
|
||||
location = CPMaxRange(paragraphRange);
|
||||
}
|
||||
while (location < CPMaxRange(paragraphsRange));
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Applies a pasted paragraph style that has text lists, or replaces the lists of the selected
|
||||
paragraphs, through the list path, so that markers follow. The pasted lists are new objects,
|
||||
decoded from the pasteboard. At each level, the list of the paragraph just before or after
|
||||
the selection takes the place of a pasted list with the same marker format and options, so
|
||||
that the pasted paragraphs join a neighbouring list and continue its numbering.
|
||||
*/
|
||||
- (void)_pasteRulerParagraphStyleWithTextLists:(CPParagraphStyle)aParagraphStyle
|
||||
{
|
||||
var paragraphsRange = [self _textListParagraphsRangeForRange:_selectionRange],
|
||||
pastedLists = [aParagraphStyle textLists],
|
||||
neighbourLists = [],
|
||||
lists = [];
|
||||
|
||||
if (paragraphsRange.location > 0)
|
||||
neighbourLists = [_textStorage _textListsOfParagraphAtIndex:[_textStorage _paragraphRangeForIndex:paragraphsRange.location - 1].location];
|
||||
|
||||
if (![neighbourLists count] && CPMaxRange(paragraphsRange) < [_textStorage length])
|
||||
neighbourLists = [_textStorage _textListsOfParagraphAtIndex:CPMaxRange(paragraphsRange)];
|
||||
|
||||
for (var level = 0; level < [pastedLists count]; level++)
|
||||
{
|
||||
var pastedList = pastedLists[level],
|
||||
neighbourList = level < [neighbourLists count] ? neighbourLists[level] : nil,
|
||||
enclosingMatches = level === 0 || lists[level - 1] === neighbourLists[level - 1];
|
||||
|
||||
if (enclosingMatches && neighbourList && [neighbourList markerFormat] === [pastedList markerFormat] && [neighbourList listOptions] === [pastedList listOptions])
|
||||
lists.push(neighbourList);
|
||||
else
|
||||
lists.push(pastedList);
|
||||
}
|
||||
|
||||
var paragraphStyle = [aParagraphStyle mutableCopy],
|
||||
listsForParagraphs = [],
|
||||
location = paragraphsRange.location;
|
||||
|
||||
[paragraphStyle setTextLists:lists];
|
||||
|
||||
do
|
||||
{
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:location];
|
||||
|
||||
listsForParagraphs.push(lists);
|
||||
location = CPMaxRange(paragraphRange);
|
||||
}
|
||||
while (location < CPMaxRange(paragraphsRange));
|
||||
|
||||
[self _setTextLists:listsForParagraphs forParagraphsInRange:paragraphsRange paragraphStyle:paragraphStyle];
|
||||
}
|
||||
|
||||
// MARK: Editing in Lists
|
||||
|
||||
/* @ignore
|
||||
Return in a list item starts the next item, with the same paragraph style. Return in an
|
||||
empty item ends the list there, or returns a nested item to the enclosing list. Returns
|
||||
\c NO if the insertion point is not in the text of a list item.
|
||||
*/
|
||||
- (BOOL)_insertNewlineInTextList
|
||||
{
|
||||
var paragraphRange = [self _textListItemRangeAtIndex:_selectionRange.location],
|
||||
markerRange = paragraphRange ? [self _textListMarkerRangeInParagraphRange:paragraphRange] : nil;
|
||||
|
||||
if (!markerRange || markerRange.location === CPNotFound || _selectionRange.location < CPMaxRange(markerRange))
|
||||
return NO;
|
||||
|
||||
var string = [_textStorage string],
|
||||
textEnd = CPMaxRange(paragraphRange);
|
||||
|
||||
if (textEnd > paragraphRange.location && string.charCodeAt(textEnd - 1) === 10)
|
||||
textEnd--;
|
||||
|
||||
if (!_selectionRange.length && CPMaxRange(markerRange) === textEnd)
|
||||
{
|
||||
[self _outdentTextListItemAtIndex:_selectionRange.location];
|
||||
return YES;
|
||||
}
|
||||
|
||||
var attributes = [_typingAttributes mutableCopy];
|
||||
|
||||
[attributes setObject:[_textStorage attribute:CPParagraphStyleAttributeName atIndex:paragraphRange.location effectiveRange:nil] forKey:CPParagraphStyleAttributeName];
|
||||
|
||||
// The new item's marker is filled in when the list is numbered, after the insertion.
|
||||
[self insertText:[[CPAttributedString alloc] initWithString:@"\n\t\t" attributes:attributes]];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Tab at the start of a list item makes it an item of a nested list. Returns \c NO if the
|
||||
insertion point is not at the start of a list item.
|
||||
*/
|
||||
- (BOOL)_insertTabInTextList
|
||||
{
|
||||
var paragraphRange = [self _textListItemRangeAtIndex:_selectionRange.location],
|
||||
markerRange = paragraphRange ? [self _textListMarkerRangeInParagraphRange:paragraphRange] : nil;
|
||||
|
||||
if (_selectionRange.length || !markerRange || markerRange.location === CPNotFound || _selectionRange.location > CPMaxRange(markerRange))
|
||||
return NO;
|
||||
|
||||
[self _indentTextListItemAtIndex:_selectionRange.location];
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Shift-Tab in an item of a nested list returns it to the enclosing list. In an item of a list
|
||||
that is not nested, it does nothing. Returns \c NO if the insertion point is not in a list item.
|
||||
*/
|
||||
- (BOOL)_insertBacktabInTextList
|
||||
{
|
||||
var paragraphRange = [self _textListItemRangeAtIndex:_selectionRange.location];
|
||||
|
||||
if (!paragraphRange)
|
||||
return NO;
|
||||
|
||||
if ([[_textStorage _textListsOfParagraphAtIndex:paragraphRange.location] count] > 1)
|
||||
[self _outdentTextListItemAtIndex:_selectionRange.location];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Delete at the start of a list item's text returns a nested item to the enclosing list, or
|
||||
removes the item from its list, with its marker. Returns \c NO if the insertion point is
|
||||
not at the start of a list item's text.
|
||||
*/
|
||||
- (BOOL)_deleteBackwardInTextList
|
||||
{
|
||||
var paragraphRange = [self _textListItemRangeAtIndex:_selectionRange.location],
|
||||
markerRange = paragraphRange ? [self _textListMarkerRangeInParagraphRange:paragraphRange] : nil;
|
||||
|
||||
if (_selectionRange.length || !markerRange || markerRange.location === CPNotFound || _selectionRange.location !== CPMaxRange(markerRange))
|
||||
return NO;
|
||||
|
||||
[self _outdentTextListItemAtIndex:_selectionRange.location];
|
||||
return YES;
|
||||
}
|
||||
|
||||
// MARK: Numbering
|
||||
|
||||
/* @ignore
|
||||
Numbers again the markers of the lists in and next to aRange, which a change has just
|
||||
affected. Only marker text changes, without undo: undoing the change restores the text, and
|
||||
the lists are numbered again afterwards. The selection keeps its place in the text.
|
||||
*/
|
||||
- (void)_updateTextListMarkersAroundRange:(CPRange)aRange
|
||||
{
|
||||
var length = [_textStorage length];
|
||||
|
||||
if (!length || ![self isRichText])
|
||||
return;
|
||||
|
||||
// The paragraphs from the one before the change to the one after it.
|
||||
var scanStart = [_textStorage _paragraphRangeForIndex:MAX(0, aRange.location - 1)].location,
|
||||
scanEnd = CPMaxRange([_textStorage _paragraphRangeForIndex:MIN(length, CPMaxRange(aRange))]),
|
||||
listRanges = [],
|
||||
location = scanStart;
|
||||
|
||||
if (scanEnd < length)
|
||||
scanEnd = CPMaxRange([_textStorage _paragraphRangeForIndex:scanEnd]);
|
||||
|
||||
while (location < scanEnd)
|
||||
{
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:location],
|
||||
lists = [_textStorage _textListsOfParagraphAtIndex:location];
|
||||
|
||||
if ([lists count])
|
||||
{
|
||||
var listRange = [_textStorage rangeOfTextList:lists[0] atIndex:location];
|
||||
|
||||
listRanges.push(listRange);
|
||||
location = MAX(CPMaxRange(listRange), CPMaxRange(paragraphRange));
|
||||
}
|
||||
else
|
||||
location = CPMaxRange(paragraphRange);
|
||||
|
||||
if (!paragraphRange.length)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!listRanges.length)
|
||||
return;
|
||||
|
||||
var replacements = [];
|
||||
|
||||
for (var i = 0; i < listRanges.length; i++)
|
||||
[self _collectTextListMarkerReplacements:replacements inRange:listRanges[i]];
|
||||
|
||||
if (!replacements.length)
|
||||
return;
|
||||
|
||||
var selectionStart = _selectionRange.location,
|
||||
selectionEnd = CPMaxRange(_selectionRange);
|
||||
|
||||
// Replace from the end, so that the earlier ranges stay valid.
|
||||
[_textStorage beginEditing];
|
||||
|
||||
for (var i = replacements.length - 1; i >= 0; i--)
|
||||
{
|
||||
var replacement = replacements[i],
|
||||
range = replacement.range,
|
||||
delta = replacement.string.length - range.length;
|
||||
|
||||
[_textStorage replaceCharactersInRange:range
|
||||
withAttributedString:[[CPAttributedString alloc] initWithString:replacement.string attributes:[_textStorage attributesAtIndex:range.location effectiveRange:nil]]];
|
||||
|
||||
if (selectionStart >= CPMaxRange(range))
|
||||
selectionStart += delta;
|
||||
else if (selectionStart > range.location)
|
||||
selectionStart = range.location + replacement.string.length;
|
||||
|
||||
if (selectionEnd >= CPMaxRange(range))
|
||||
selectionEnd += delta;
|
||||
else if (selectionEnd > range.location)
|
||||
selectionEnd = range.location + replacement.string.length;
|
||||
}
|
||||
|
||||
[_textStorage endEditing];
|
||||
|
||||
[self _setSelectedRange:CPMakeRange(selectionStart, selectionEnd - selectionStart) affinity:0 stillSelecting:NO overwriteTypingAttributes:NO];
|
||||
[_layoutManager _validateLayoutAndGlyphs];
|
||||
[self sizeToFit];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Adds to someReplacements the marker of each item in aListRange whose text differs from its
|
||||
number. The items are counted in one pass, with a counter for each list. As in Cocoa, a list
|
||||
with the CPTextListPrependEnclosingMarker option puts the enclosing item's marker before its
|
||||
own.
|
||||
*/
|
||||
- (void)_collectTextListMarkerReplacements:(CPArray)someReplacements inRange:(CPRange)aListRange
|
||||
{
|
||||
var string = [_textStorage string],
|
||||
counters = {},
|
||||
location = aListRange.location;
|
||||
|
||||
while (location < CPMaxRange(aListRange))
|
||||
{
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:location],
|
||||
lists = [_textStorage _textListsOfParagraphAtIndex:location],
|
||||
count = [lists count],
|
||||
innermostUID = [lists[count - 1] UID],
|
||||
marker = @"";
|
||||
|
||||
counters[innermostUID] = (counters.hasOwnProperty(innermostUID) ? counters[innermostUID] : [lists[count - 1] startingItemNumber] - 1) + 1;
|
||||
|
||||
for (var level = 0; level < count; level++)
|
||||
{
|
||||
var list = lists[level],
|
||||
uid = [list UID],
|
||||
itemNumber = counters.hasOwnProperty(uid) ? counters[uid] : [list startingItemNumber],
|
||||
levelMarker = [list markerForItemNumber:itemNumber];
|
||||
|
||||
marker = ([list listOptions] & CPTextListPrependEnclosingMarker) ? marker + levelMarker : levelMarker;
|
||||
}
|
||||
|
||||
var markerRange = [self _textListMarkerRangeInParagraphRange:paragraphRange],
|
||||
newMarker = "\t" + marker + "\t";
|
||||
|
||||
if (markerRange.location !== CPNotFound && string.substr(markerRange.location, markerRange.length) !== newMarker)
|
||||
someReplacements.push({range: markerRange, string: newMarker});
|
||||
|
||||
if (!paragraphRange.length)
|
||||
break;
|
||||
|
||||
location = CPMaxRange(paragraphRange);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
/* @ignore
|
||||
Returns the range of the list item paragraph at anIndex, or \c nil if the paragraph is not
|
||||
a list item or the text is not rich.
|
||||
*/
|
||||
- (CPRange)_textListItemRangeAtIndex:(unsigned)anIndex
|
||||
{
|
||||
if (![self isRichText])
|
||||
return nil;
|
||||
|
||||
var paragraphRange = [_textStorage _paragraphRangeForIndex:anIndex];
|
||||
|
||||
// At the end of the text after a paragraph separator, the insertion point is in an empty
|
||||
// paragraph that has no attributes of its own.
|
||||
if (![[_textStorage _textListsOfParagraphAtIndex:paragraphRange.location] count])
|
||||
return nil;
|
||||
|
||||
return paragraphRange;
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the range of the marker at the start of a list item: a tab, the marker and another
|
||||
tab. Returns a range with the location CPNotFound if the paragraph is not a list item, or
|
||||
its text does not start with a marker.
|
||||
*/
|
||||
- (CPRange)_textListMarkerRangeInParagraphRange:(CPRange)aParagraphRange
|
||||
{
|
||||
var string = [_textStorage string],
|
||||
start = aParagraphRange.location,
|
||||
end = CPMaxRange(aParagraphRange);
|
||||
|
||||
if (![[_textStorage _textListsOfParagraphAtIndex:start] count] || string.charCodeAt(start) !== 9)
|
||||
return CPMakeRange(CPNotFound, 0);
|
||||
|
||||
for (var i = start + 1; i < end; i++)
|
||||
{
|
||||
var charCode = string.charCodeAt(i);
|
||||
|
||||
if (charCode === 9)
|
||||
return CPMakeRange(start, i - start + 1);
|
||||
|
||||
if (charCode === 10 || charCode === 13 || charCode === 0x2029)
|
||||
break;
|
||||
}
|
||||
|
||||
return CPMakeRange(CPNotFound, 0);
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the range of whole paragraphs that aRange touches. A selection that ends at the
|
||||
start of a paragraph does not include that paragraph.
|
||||
*/
|
||||
- (CPRange)_textListParagraphsRangeForRange:(CPRange)aRange
|
||||
{
|
||||
var first = [_textStorage _paragraphRangeForIndex:aRange.location],
|
||||
lastIndex = aRange.length ? CPMaxRange(aRange) - 1 : aRange.location,
|
||||
last = [_textStorage _paragraphRangeForIndex:MAX(first.location, lastIndex)];
|
||||
|
||||
return CPMakeRange(first.location, CPMaxRange(last) - first.location);
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns the attributes for a list item's marker and paragraph style: those of the first
|
||||
character of its text, or of the paragraph, or the typing attributes for an empty paragraph.
|
||||
*/
|
||||
- (CPDictionary)_textListAttributesForParagraphRange:(CPRange)aParagraphRange textStart:(unsigned)aTextStart
|
||||
{
|
||||
var length = [_textStorage length];
|
||||
|
||||
if (aTextStart < CPMaxRange(aParagraphRange) && aTextStart < length && [_textStorage string].charCodeAt(aTextStart) !== 10)
|
||||
return [_textStorage attributesAtIndex:aTextStart effectiveRange:nil];
|
||||
|
||||
if (aParagraphRange.location < length)
|
||||
return [_textStorage attributesAtIndex:aParagraphRange.location effectiveRange:nil];
|
||||
|
||||
return [_typingAttributes mutableCopy];
|
||||
}
|
||||
|
||||
/* @ignore
|
||||
Returns a copy of aParagraphStyle for the text lists someLists. The indents and tab stops of
|
||||
a list item place its marker CPTextViewTextListMarkerIndent points into its level, and its
|
||||
text, including later lines, at the start of the next level. A paragraph without lists gets
|
||||
no indents and the default tab stops.
|
||||
*/
|
||||
- (CPParagraphStyle)_paragraphStyle:(CPParagraphStyle)aParagraphStyle withTextLists:(CPArray)someLists
|
||||
{
|
||||
var paragraphStyle = [(aParagraphStyle || [CPParagraphStyle defaultParagraphStyle]) mutableCopy],
|
||||
level = [someLists count];
|
||||
|
||||
[paragraphStyle setTextLists:someLists];
|
||||
|
||||
if (level)
|
||||
{
|
||||
var levelStart = (level - 1) * CPTextViewTextListLevelIndent,
|
||||
textStart = level * CPTextViewTextListLevelIndent;
|
||||
|
||||
[paragraphStyle setFirstLineHeadIndent:levelStart];
|
||||
[paragraphStyle setHeadIndent:textStart];
|
||||
[paragraphStyle setTabStops:[[[CPTextTab alloc] initWithType:CPLeftTabStopType location:levelStart + CPTextViewTextListMarkerIndent],
|
||||
[[CPTextTab alloc] initWithType:CPLeftTabStopType location:textStart]]];
|
||||
}
|
||||
else
|
||||
{
|
||||
[paragraphStyle setFirstLineHeadIndent:0.0];
|
||||
[paragraphStyle setHeadIndent:0.0];
|
||||
[paragraphStyle setTabStops:[[CPParagraphStyle defaultParagraphStyle] tabStops]];
|
||||
}
|
||||
|
||||
return paragraphStyle;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPTextView (CPTextViewDelegate)
|
||||
|
||||
- (BOOL)_delegateRespondsToShouldChangeTypingAttributesToAttributes
|
||||
|
||||
@@ -2007,10 +2007,11 @@ CPTexturedBackgroundWindowMask
|
||||
return [[self firstResponder] keyUp:anEvent];
|
||||
|
||||
case CPKeyDown:
|
||||
// Tab moves to the next key view, unless the first responder takes plain Tab itself, as an
|
||||
// editable text view does to insert a tab character. Control-Tab always moves on, as in Cocoa.
|
||||
// Tab and Shift-Tab move to the next and previous key view, unless the first responder takes
|
||||
// them itself, as an editable text view does to insert a tab character or change a list
|
||||
// item's level. Control-Tab always moves on, as in Cocoa.
|
||||
var modifierFlags = [anEvent modifierFlags],
|
||||
firstResponderTakesTab = !(modifierFlags & (CPShiftKeyMask | CPControlKeyMask)) && [[self firstResponder] _wantsTabKey];
|
||||
firstResponderTakesTab = !(modifierFlags & CPControlKeyMask) && [[self firstResponder] _wantsTabKey];
|
||||
|
||||
if ([anEvent charactersIgnoringModifiers] === CPTabCharacter &&
|
||||
!(modifierFlags & (CPAlternateKeyMask | CPCommandKeyMask)) &&
|
||||
@@ -2029,7 +2030,7 @@ CPTexturedBackgroundWindowMask
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if ([anEvent charactersIgnoringModifiers] === CPBackTabCharacter)
|
||||
else if ([anEvent charactersIgnoringModifiers] === CPBackTabCharacter && !firstResponderTakesTab)
|
||||
{
|
||||
var didTabBack = [self selectPreviousKeyView:self];
|
||||
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
@import <AppKit/CPApplication.j>
|
||||
@import <AppKit/CPTextList.j>
|
||||
@import <AppKit/CPTextView.j>
|
||||
|
||||
@implementation CPTextListTest : OJTestCase
|
||||
{
|
||||
CPWindow theWindow;
|
||||
CPTextView textView;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 1024.0, 768.0) styleMask:CPWindowNotSizable];
|
||||
textView = [[CPTextView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
|
||||
|
||||
[textView setRichText:YES];
|
||||
[[theWindow contentView] addSubview:textView];
|
||||
[theWindow makeFirstResponder:textView];
|
||||
}
|
||||
|
||||
- (id)roundTrip:(id)anObject
|
||||
{
|
||||
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:anObject]];
|
||||
}
|
||||
|
||||
// MARK: CPTextList
|
||||
|
||||
- (void)testBulletMarkers
|
||||
{
|
||||
[self assert:@"•" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerDisc options:0] markerForItemNumber:3]];
|
||||
[self assert:@"◦" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerCircle options:0] markerForItemNumber:1]];
|
||||
[self assert:@"⁃" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerHyphen options:0] markerForItemNumber:1]];
|
||||
}
|
||||
|
||||
- (void)testOrderedMarkers
|
||||
{
|
||||
[self assert:@"12." equals:[[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0] markerForItemNumber:12]];
|
||||
[self assert:@"(c)" equals:[[[CPTextList alloc] initWithMarkerFormat:@"({lower-alpha})" options:0] markerForItemNumber:3]];
|
||||
[self assert:@"AA" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerUppercaseAlpha options:0] markerForItemNumber:27]];
|
||||
[self assert:@"xiv" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerLowercaseRoman options:0] markerForItemNumber:14]];
|
||||
[self assert:@"MCMXCIX" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerUppercaseRoman options:0] markerForItemNumber:1999]];
|
||||
[self assert:@"FF" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerUppercaseHexadecimal options:0] markerForItemNumber:255]];
|
||||
[self assert:@"17" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerOctal options:0] markerForItemNumber:15]];
|
||||
}
|
||||
|
||||
- (void)testIsOrdered
|
||||
{
|
||||
[self assertFalse:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerSquare options:0] isOrdered]];
|
||||
[self assertTrue:[[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0] isOrdered]];
|
||||
}
|
||||
|
||||
- (void)testTextListArchiving
|
||||
{
|
||||
var list = [[CPTextList alloc] initWithMarkerFormat:@"{upper-roman}." options:CPTextListPrependEnclosingMarker startingItemNumber:4],
|
||||
decodedList = [self roundTrip:list];
|
||||
|
||||
[self assert:@"{upper-roman}." equals:[decodedList markerFormat]];
|
||||
[self assert:CPTextListPrependEnclosingMarker equals:[decodedList listOptions]];
|
||||
[self assert:4 equals:[decodedList startingItemNumber]];
|
||||
}
|
||||
|
||||
- (void)testParagraphStyleTextLists
|
||||
{
|
||||
var list = [[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerDisc options:0],
|
||||
paragraphStyle = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
|
||||
|
||||
[self assert:0 equals:[[paragraphStyle textLists] count]];
|
||||
|
||||
[paragraphStyle setTextLists:[list]];
|
||||
[self assert:list equals:[[[paragraphStyle copy] textLists] firstObject]];
|
||||
[self assertFalse:[paragraphStyle isEqual:[CPParagraphStyle defaultParagraphStyle]] message:@"a paragraph style with a list differs from one without"];
|
||||
[self assert:@"{disc}" equals:[[[[self roundTrip:paragraphStyle] textLists] firstObject] markerFormat]];
|
||||
}
|
||||
|
||||
// MARK: CPAttributedString
|
||||
|
||||
- (void)testRangeAndItemNumberOfTextList
|
||||
{
|
||||
var outer = [[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0],
|
||||
inner = [[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerDisc options:0],
|
||||
outerStyle = [[CPParagraphStyle defaultParagraphStyle] mutableCopy],
|
||||
innerStyle = [[CPParagraphStyle defaultParagraphStyle] mutableCopy],
|
||||
string = [[CPAttributedString alloc] initWithString:@"Intro\none\ntwo\nnested\nthree\nEnd"];
|
||||
|
||||
[outerStyle setTextLists:[outer]];
|
||||
[innerStyle setTextLists:[outer, inner]];
|
||||
|
||||
[string addAttribute:CPParagraphStyleAttributeName value:outerStyle range:CPMakeRange(6, 8)];
|
||||
[string addAttribute:CPParagraphStyleAttributeName value:innerStyle range:CPMakeRange(14, 7)];
|
||||
[string addAttribute:CPParagraphStyleAttributeName value:outerStyle range:CPMakeRange(21, 6)];
|
||||
|
||||
var outerRange = [string rangeOfTextList:outer atIndex:15];
|
||||
|
||||
[self assert:6 equals:outerRange.location];
|
||||
[self assert:21 equals:outerRange.length];
|
||||
[self assert:CPNotFound equals:[string rangeOfTextList:outer atIndex:2].location];
|
||||
|
||||
[self assert:1 equals:[string itemNumberInTextList:outer atIndex:7]];
|
||||
[self assert:2 equals:[string itemNumberInTextList:outer atIndex:11]];
|
||||
[self assert:3 equals:[string itemNumberInTextList:outer atIndex:23] message:@"nested items do not count in the enclosing list"];
|
||||
[self assert:1 equals:[string itemNumberInTextList:inner atIndex:16]];
|
||||
}
|
||||
|
||||
// MARK: CPTextView
|
||||
|
||||
- (void)testSetTextListAddsAndRemovesMarkers
|
||||
{
|
||||
[textView setString:@"one\ntwo"];
|
||||
[textView setSelectedRange:CPMakeRange(0, 7)];
|
||||
[textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]];
|
||||
|
||||
[self assert:@"\t1.\tone\n\t2.\ttwo" equals:[textView string]];
|
||||
[self assert:36.0 equals:[[[textView textStorage] attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil] headIndent]];
|
||||
|
||||
[textView setSelectedRange:CPMakeRange(0, [[textView string] length])];
|
||||
[textView setTextList:nil];
|
||||
|
||||
[self assert:@"one\ntwo" equals:[textView string]];
|
||||
}
|
||||
|
||||
- (void)testNewlineContinuesAndEndsList
|
||||
{
|
||||
[textView setString:@"one"];
|
||||
[textView setSelectedRange:CPMakeRange(3, 0)];
|
||||
[textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]];
|
||||
|
||||
[textView setSelectedRange:CPMakeRange([[textView string] length], 0)];
|
||||
[textView insertNewline:nil];
|
||||
[textView insertText:@"two"];
|
||||
|
||||
[self assert:@"\t1.\tone\n\t2.\ttwo" equals:[textView string]];
|
||||
|
||||
[textView insertNewline:nil];
|
||||
[textView insertNewline:nil];
|
||||
|
||||
[self assert:@"\t1.\tone\n\t2.\ttwo\n" equals:[textView string] message:@"Return in an empty item ends the list"];
|
||||
}
|
||||
|
||||
- (void)testTabNestsAndBacktabReturnsItem
|
||||
{
|
||||
[textView setString:@"one\ntwo\nthree"];
|
||||
[textView setSelectedRange:CPMakeRange(0, 13)];
|
||||
[textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]];
|
||||
|
||||
// The insertion point at the start of the second item's text.
|
||||
var secondItemText = [[textView string] rangeOfString:@"two"].location;
|
||||
|
||||
[textView setSelectedRange:CPMakeRange(secondItemText, 0)];
|
||||
[textView insertTab:nil];
|
||||
|
||||
[self assert:@"\t1.\tone\n\t1.\ttwo\n\t2.\tthree" equals:[textView string]];
|
||||
[self assert:2 equals:[[[[textView textStorage] attribute:CPParagraphStyleAttributeName atIndex:secondItemText effectiveRange:nil] textLists] count]];
|
||||
|
||||
[textView insertBacktab:nil];
|
||||
|
||||
[self assert:@"\t1.\tone\n\t2.\ttwo\n\t3.\tthree" equals:[textView string]];
|
||||
}
|
||||
|
||||
- (void)testDeleteBackwardAtItemStartRemovesItem
|
||||
{
|
||||
[textView setString:@"one\ntwo"];
|
||||
[textView setSelectedRange:CPMakeRange(0, 7)];
|
||||
[textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]];
|
||||
|
||||
[textView setSelectedRange:CPMakeRange([[textView string] rangeOfString:@"one"].location, 0)];
|
||||
[textView deleteBackward:nil];
|
||||
|
||||
[self assert:@"one\n\t1.\ttwo" equals:[textView string] message:@"the remaining item is numbered again"];
|
||||
}
|
||||
|
||||
- (void)testPrependEnclosingMarker
|
||||
{
|
||||
[textView setString:@"one\ntwo"];
|
||||
[textView setSelectedRange:CPMakeRange(0, 7)];
|
||||
[textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:CPTextListPrependEnclosingMarker]];
|
||||
|
||||
[textView setSelectedRange:CPMakeRange([[textView string] rangeOfString:@"two"].location, 0)];
|
||||
[textView insertTab:nil];
|
||||
|
||||
[self assert:@"\t1.\tone\n\t1.1.\ttwo" equals:[textView string]];
|
||||
}
|
||||
|
||||
- (void)testPasteRulerAddsAndRemovesMarkers
|
||||
{
|
||||
[textView setString:@"one\ntwo\nthree"];
|
||||
[textView setSelectedRange:CPMakeRange(0, 0)];
|
||||
[textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]];
|
||||
|
||||
// Copy the first item's ruler and paste it on the next paragraph, which joins the list.
|
||||
[textView setSelectedRange:CPMakeRange(5, 0)];
|
||||
[textView copyRuler:nil];
|
||||
[textView setSelectedRange:CPMakeRange([[textView string] rangeOfString:@"two"].location, 0)];
|
||||
[textView pasteRuler:nil];
|
||||
|
||||
[self assert:@"\t1.\tone\n\t2.\ttwo\nthree" equals:[textView string]];
|
||||
|
||||
// Copy the plain paragraph's ruler and paste it on the first item, which leaves the list.
|
||||
[textView setSelectedRange:CPMakeRange([[textView string] length], 0)];
|
||||
[textView copyRuler:nil];
|
||||
[textView setSelectedRange:CPMakeRange(5, 0)];
|
||||
[textView pasteRuler:nil];
|
||||
|
||||
[self assert:@"one\n\t1.\ttwo\nthree" equals:[textView string]];
|
||||
}
|
||||
|
||||
- (void)testChangeTextListFromMenuItem
|
||||
{
|
||||
var numberedItem = [[CPMenuItem alloc] initWithTitle:@"1. 2. 3." action:@selector(changeTextList:) keyEquivalent:@""],
|
||||
noneItem = [[CPMenuItem alloc] initWithTitle:@"None" action:@selector(changeTextList:) keyEquivalent:@""];
|
||||
|
||||
[numberedItem setTag:10];
|
||||
|
||||
[textView setString:@"one\ntwo"];
|
||||
[textView setSelectedRange:CPMakeRange(0, 0)];
|
||||
|
||||
[self assertTrue:[textView validateUserInterfaceItem:noneItem]];
|
||||
[self assert:CPOnState equals:[noneItem state]];
|
||||
|
||||
[textView changeTextList:numberedItem];
|
||||
[self assert:@"\t1.\tone\ntwo" equals:[textView string]];
|
||||
|
||||
[textView validateUserInterfaceItem:numberedItem];
|
||||
[textView validateUserInterfaceItem:noneItem];
|
||||
[self assert:CPOnState equals:[numberedItem state]];
|
||||
[self assert:CPOffState equals:[noneItem state]];
|
||||
|
||||
// The next paragraph joins the list above it, so its numbering continues.
|
||||
[textView setSelectedRange:CPMakeRange([[textView string] length], 0)];
|
||||
[textView changeTextList:numberedItem];
|
||||
[self assert:@"\t1.\tone\n\t2.\ttwo" equals:[textView string]];
|
||||
|
||||
[textView setRichText:NO];
|
||||
[self assertFalse:[textView validateUserInterfaceItem:numberedItem]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -208,6 +208,25 @@
|
||||
[self assertTrue:([[textView typingAttributes] objectForKey:CPSuperscriptAttributeName] == nil)];
|
||||
}
|
||||
|
||||
- (void)testTypingAttributesComeFromTheCharacterBeforeTheInsertionPoint
|
||||
{
|
||||
var font = [CPFont boldSystemFontOfSize:20.0],
|
||||
length = [stringValue length];
|
||||
|
||||
[textView setRichText:YES];
|
||||
[self assertTrue:[theWindow makeFirstResponder:textView]];
|
||||
[[textView textStorage] addAttribute:CPFontAttributeName value:font range:CPMakeRange(length - 4, 4)];
|
||||
|
||||
[textView setSelectedRange:CPMakeRange(length, 0)];
|
||||
[self assert:[font cssString] equals:[[[textView typingAttributes] objectForKey:CPFontAttributeName] cssString] message:@"at the end of the text"];
|
||||
|
||||
[textView setSelectedRange:CPMakeRange(length - 4, 0)];
|
||||
[self assertFalse:[font cssString] === [[[textView typingAttributes] objectForKey:CPFontAttributeName] cssString] message:@"before the styled text"];
|
||||
|
||||
[textView setSelectedRange:CPMakeRange(length - 4, 2)];
|
||||
[self assert:[font cssString] equals:[[[textView typingAttributes] objectForKey:CPFontAttributeName] cssString] message:@"for a selection"];
|
||||
}
|
||||
|
||||
- (void)testCopyAndPasteStyle
|
||||
{
|
||||
var font = [CPFont boldSystemFontOfSize:20.0],
|
||||
|
||||
@@ -1 +1 @@
|
||||
280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;0E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;4E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;0E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;1;7E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;1;9E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;11E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;13E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;15E;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;18E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;20E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;22E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;27E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;33E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;38E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;44E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;47E;D;K;6;CP$UIDd;2;48E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;50E;D;K;6;CP$UIDd;2;51E;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;2;53E;D;K;6;CP$UIDd;2;54E;D;K;6;CP$UIDd;2;55E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;57E;D;K;6;CP$UIDd;2;58E;D;K;6;CP$UIDd;2;59E;D;K;6;CP$UIDd;2;60E;D;K;6;CP$UIDd;2;61E;D;K;6;CP$UIDd;2;62E;D;K;6;CP$UIDd;2;63E;D;K;6;CP$UIDd;2;64E;D;K;6;CP$UIDd;2;65E;D;K;6;CP$UIDd;2;66E;D;K;6;CP$UIDd;2;67E;D;K;6;CP$UIDd;2;68E;D;K;6;CP$UIDd;2;69E;D;K;6;CP$UIDd;2;70E;D;K;6;CP$UIDd;2;71E;D;K;6;CP$UIDd;2;72E;D;K;6;CP$UIDd;2;73E;D;K;6;CP$UIDd;2;74E;D;K;6;CP$UIDd;2;75E;D;K;6;CP$UIDd;2;76E;D;K;6;CP$UIDd;2;77E;D;K;6;CP$UIDd;2;78E;D;K;6;CP$UIDd;2;79E;D;K;6;CP$UIDd;2;80E;D;K;6;CP$UIDd;2;81E;D;K;6;CP$UIDd;2;82E;D;K;6;CP$UIDd;2;83E;D;K;6;CP$UIDd;2;84E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;85E;D;K;6;CP$UIDd;2;87E;D;K;6;CP$UIDd;2;89E;D;K;6;CP$UIDd;2;90E;D;K;6;CP$UIDd;2;91E;D;K;6;CP$UIDd;2;92E;D;K;6;CP$UIDd;2;93E;D;K;6;CP$UIDd;2;94E;D;K;6;CP$UIDd;2;95E;D;K;6;CP$UIDd;2;96E;D;K;6;CP$UIDd;2;97E;D;K;6;CP$UIDd;2;98E;D;K;6;CP$UIDd;2;99E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;101E;D;K;6;CP$UIDd;3;102E;D;K;6;CP$UIDd;3;103E;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;105E;D;K;6;CP$UIDd;3;106E;D;K;6;CP$UIDd;3;107E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;109E;D;K;6;CP$UIDd;3;110E;D;K;6;CP$UIDd;3;111E;D;K;6;CP$UIDd;3;112E;D;K;6;CP$UIDd;3;113E;D;K;6;CP$UIDd;3;114E;D;K;6;CP$UIDd;3;115E;D;K;6;CP$UIDd;3;116E;D;K;6;CP$UIDd;3;117E;D;K;6;CP$UIDd;3;118E;D;K;6;CP$UIDd;3;119E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;121E;D;K;6;CP$UIDd;3;122E;D;K;6;CP$UIDd;3;123E;D;K;6;CP$UIDd;3;124E;D;K;6;CP$UIDd;3;125E;D;K;6;CP$UIDd;3;126E;D;K;6;CP$UIDd;3;127E;D;K;6;CP$UIDd;3;128E;D;K;6;CP$UIDd;3;129E;D;K;6;CP$UIDd;3;130E;D;K;6;CP$UIDd;3;131E;D;K;6;CP$UIDd;3;132E;D;K;6;CP$UIDd;3;133E;D;K;6;CP$UIDd;3;134E;D;K;6;CP$UIDd;3;135E;D;K;6;CP$UIDd;3;136E;D;K;6;CP$UIDd;3;137E;D;K;6;CP$UIDd;3;138E;D;K;6;CP$UIDd;3;139E;D;K;6;CP$UIDd;3;140E;D;K;6;CP$UIDd;3;141E;D;K;6;CP$UIDd;3;142E;D;K;6;CP$UIDd;3;143E;D;K;6;CP$UIDd;3;144E;D;K;6;CP$UIDd;3;145E;D;K;6;CP$UIDd;3;146E;D;K;6;CP$UIDd;3;147E;D;K;6;CP$UIDd;3;148E;D;K;6;CP$UIDd;3;149E;D;K;6;CP$UIDd;3;150E;D;K;6;CP$UIDd;3;151E;D;K;6;CP$UIDd;3;152E;D;K;6;CP$UIDd;3;153E;D;K;6;CP$UIDd;3;154E;D;K;6;CP$UIDd;3;155E;D;K;6;CP$UIDd;3;156E;D;K;6;CP$UIDd;3;157E;D;K;6;CP$UIDd;3;158E;D;K;6;CP$UIDd;3;159E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;161E;D;K;6;CP$UIDd;3;162E;D;K;6;CP$UIDd;3;163E;D;K;6;CP$UIDd;3;164E;D;K;6;CP$UIDd;3;165E;D;K;6;CP$UIDd;3;166E;D;K;6;CP$UIDd;3;167E;D;K;6;CP$UIDd;3;168E;D;K;6;CP$UIDd;3;169E;D;K;6;CP$UIDd;3;170E;D;K;6;CP$UIDd;3;171E;D;K;6;CP$UIDd;3;172E;D;K;6;CP$UIDd;3;173E;D;K;6;CP$UIDd;3;174E;D;K;6;CP$UIDd;3;175E;D;K;6;CP$UIDd;3;176E;D;K;6;CP$UIDd;3;177E;D;K;6;CP$UIDd;3;178E;D;K;6;CP$UIDd;3;179E;D;K;6;CP$UIDd;3;180E;D;K;6;CP$UIDd;3;181E;D;K;6;CP$UIDd;3;182E;D;K;6;CP$UIDd;3;183E;D;K;6;CP$UIDd;3;184E;D;K;6;CP$UIDd;3;185E;D;K;6;CP$UIDd;3;186E;D;K;6;CP$UIDd;3;187E;D;K;6;CP$UIDd;3;188E;D;K;6;CP$UIDd;3;189E;D;K;6;CP$UIDd;3;190E;D;K;6;CP$UIDd;3;191E;D;K;6;CP$UIDd;3;192E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;194E;D;K;6;CP$UIDd;3;195E;D;K;6;CP$UIDd;3;196E;D;K;6;CP$UIDd;3;197E;D;K;6;CP$UIDd;3;198E;D;K;6;CP$UIDd;3;199E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;201E;D;K;6;CP$UIDd;3;202E;D;K;6;CP$UIDd;3;203E;D;K;6;CP$UIDd;3;204E;D;K;6;CP$UIDd;3;205E;D;Line truncated
|
||||
280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;0E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;4E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;0E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;1;7E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;1;9E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;11E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;13E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;15E;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;18E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;20E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;22E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;27E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;33E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;38E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;44E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;47E;D;K;6;CP$UIDd;2;48E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;50E;D;K;6;CP$UIDd;2;51E;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;2;53E;D;K;6;CP$UIDd;2;54E;D;K;6;CP$UIDd;2;55E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;57E;D;K;6;CP$UIDd;2;58E;D;K;6;CP$UIDd;2;59E;D;K;6;CP$UIDd;2;60E;D;K;6;CP$UIDd;2;61E;D;K;6;CP$UIDd;2;62E;D;K;6;CP$UIDd;2;63E;D;K;6;CP$UIDd;2;64E;D;K;6;CP$UIDd;2;65E;D;K;6;CP$UIDd;2;66E;D;K;6;CP$UIDd;2;67E;D;K;6;CP$UIDd;2;68E;D;K;6;CP$UIDd;2;69E;D;K;6;CP$UIDd;2;70E;D;K;6;CP$UIDd;2;71E;D;K;6;CP$UIDd;2;72E;D;K;6;CP$UIDd;2;73E;D;K;6;CP$UIDd;2;74E;D;K;6;CP$UIDd;2;75E;D;K;6;CP$UIDd;2;76E;D;K;6;CP$UIDd;2;77E;D;K;6;CP$UIDd;2;78E;D;K;6;CP$UIDd;2;79E;D;K;6;CP$UIDd;2;80E;D;K;6;CP$UIDd;2;81E;D;K;6;CP$UIDd;2;82E;D;K;6;CP$UIDd;2;83E;D;K;6;CP$UIDd;2;84E;D;K;6;CP$UIDd;2;85E;D;K;6;CP$UIDd;2;86E;D;K;6;CP$UIDd;2;87E;D;K;6;CP$UIDd;2;88E;D;K;6;CP$UIDd;2;89E;D;K;6;CP$UIDd;2;90E;D;K;6;CP$UIDd;2;91E;D;K;6;CP$UIDd;2;92E;D;K;6;CP$UIDd;2;93E;D;K;6;CP$UIDd;2;94E;D;K;6;CP$UIDd;2;95E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;96E;D;K;6;CP$UIDd;2;98E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;101E;D;K;6;CP$UIDd;3;102E;D;K;6;CP$UIDd;3;103E;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;105E;D;K;6;CP$UIDd;3;106E;D;K;6;CP$UIDd;3;107E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;109E;D;K;6;CP$UIDd;3;110E;D;K;6;CP$UIDd;3;111E;D;K;6;CP$UIDd;3;112E;D;K;6;CP$UIDd;3;113E;D;K;6;CP$UIDd;3;114E;D;K;6;CP$UIDd;3;115E;D;K;6;CP$UIDd;3;116E;D;K;6;CP$UIDd;3;117E;D;K;6;CP$UIDd;3;118E;D;K;6;CP$UIDd;3;119E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;121E;D;K;6;CP$UIDd;3;122E;D;K;6;CP$UIDd;3;123E;D;K;6;CP$UIDd;3;124E;D;K;6;CP$UIDd;3;125E;D;K;6;CP$UIDd;3;126E;D;K;6;CP$UIDd;3;127E;D;K;6;CP$UIDd;3;128E;D;K;6;CP$UIDd;3;129E;D;K;6;CP$UIDd;3;130E;D;K;6;CP$UIDd;3;131E;D;K;6;CP$UIDd;3;132E;D;K;6;CP$UIDd;3;133E;D;K;6;CP$UIDd;3;134E;D;K;6;CP$UIDd;3;135E;D;K;6;CP$UIDd;3;136E;D;K;6;CP$UIDd;3;137E;D;K;6;CP$UIDd;3;138E;D;K;6;CP$UIDd;3;139E;D;K;6;CP$UIDd;3;140E;D;K;6;CP$UIDd;3;141E;D;K;6;CP$UIDd;3;142E;D;K;6;CP$UIDd;3;143E;D;K;6;CP$UIDd;3;144E;D;K;6;CP$UIDd;3;145E;D;K;6;CP$UIDd;3;146E;D;K;6;CP$UIDd;3;147E;D;K;6;CP$UIDd;3;148E;D;K;6;CP$UIDd;3;149E;D;K;6;CP$UIDd;3;150E;D;K;6;CP$UIDd;3;151E;D;K;6;CP$UIDd;3;152E;D;K;6;CP$UIDd;3;153E;D;K;6;CP$UIDd;3;154E;D;K;6;CP$UIDd;3;155E;D;K;6;CP$UIDd;3;156E;D;K;6;CP$UIDd;3;157E;D;K;6;CP$UIDd;3;158E;D;K;6;CP$UIDd;3;159E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;161E;D;K;6;CP$UIDd;3;162E;D;K;6;CP$UIDd;3;163E;D;K;6;CP$UIDd;3;164E;D;K;6;CP$UIDd;3;165E;D;K;6;CP$UIDd;3;166E;D;K;6;CP$UIDd;3;167E;D;K;6;CP$UIDd;3;168E;D;K;6;CP$UIDd;3;169E;D;K;6;CP$UIDd;3;170E;D;K;6;CP$UIDd;3;171E;D;K;6;CP$UIDd;3;172E;D;K;6;CP$UIDd;3;173E;D;K;6;CP$UIDd;3;174E;D;K;6;CP$UIDd;3;175E;D;K;6;CP$UIDd;3;176E;D;K;6;CP$UIDd;3;177E;D;K;6;CP$UIDd;3;178E;D;K;6;CP$UIDd;3;179E;D;K;6;CP$UIDd;3;180E;D;K;6;CP$UIDd;3;181E;D;K;6;CP$UIDd;3;182E;D;K;6;CP$UIDd;3;183E;D;K;6;CP$UIDd;3;184E;D;K;6;CP$UIDd;3;185E;D;K;6;CP$UIDd;3;186E;D;K;6;CP$UIDd;3;187E;D;K;6;CP$UIDd;3;188E;D;K;6;CP$UIDd;3;189E;D;K;6;CP$UIDd;3;190E;D;K;6;CP$UIDd;3;191E;D;K;6;CP$UIDd;3;192E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;194E;D;K;6;CP$UIDd;3;195E;D;K;6;CP$UIDd;3;196E;D;K;6;CP$UIDd;3;197E;D;K;6;CP$UIDd;3;198E;D;K;6;CP$UIDd;3;199E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;201E;D;K;6;CP$UIDd;3;202E;D;K;6;CP$UIDd;3;203E;D;K;6;CP$UIDd;3;204E;D;K;6;CP$UIDd;3;205E;D;Line truncated
|
||||
@@ -489,6 +489,85 @@
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="c67-9o-y2K">
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</menuItem>
|
||||
<menuItem title="List" id="Rex-O9-TeZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="List" id="gKv-RZ-XKr">
|
||||
<items>
|
||||
<menuItem title="None" id="lqg-eV-ubr">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="aoD-m2-Fxo"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="YqH-ki-hw7"/>
|
||||
<menuItem title="• Bullet" tag="1" id="OS2-09-IyA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="Czq-4q-hOe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="◦ Circle" tag="2" id="esv-7Z-gZe">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="UFt-wO-mDe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="▪ Square" tag="3" id="e5f-jL-ROB">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="9tM-O8-Bdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="⁃ Hyphen" tag="4" id="Avr-zr-dEJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="wQu-aW-bEz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="dVV-mi-86E"/>
|
||||
<menuItem title="1. 2. 3." tag="10" id="Yde-5F-lEI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="6Wi-PE-1Cu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="a. b. c." tag="11" id="O7p-tZ-AKi">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="HQg-mn-alg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="A. B. C." tag="12" id="P3D-s3-GxT">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="kFB-VA-TwH"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="i. ii. iii." tag="13" id="7Lg-rN-dp1">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="Zsx-hh-0L4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="I. II. III." tag="14" id="8mi-WT-TnE">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="YkH-CV-4ju"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="DXf-Xw-3FD"/>
|
||||
<menuItem title="1. 1.1. 1.1.1." tag="20" id="Mr8-pa-GEh">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="changeTextList:" target="-1" id="xIq-hL-gcz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
|
||||
@@ -22,7 +22,7 @@ TextEditor is a document-based test application and a tutorial. When the app use
|
||||
### Text features not yet supported
|
||||
|
||||
- **Images in text.** AppKit has no general `CPTextAttachment` class, only the table attachment `_CPTableTextAttachment`. The RTF parser and producer do not handle `\pict`.
|
||||
- **Lists.** There is no `CPTextList`, and `CPParagraphStyle` has no `textLists`.
|
||||
- **Lists are not read from or written to RTF.** `CPTextList` and `textLists` in `CPParagraphStyle` now exist, but the RTF parser and producer do not handle `\listtable`, `\listoverridetable`, `\ls` and `\ilvl`. A list copied as RTF keeps its marker text and indents, but loses its list structure. CPMarkdownParser also writes "- item" as the literal text " • item", with no list.
|
||||
|
||||
### Text view
|
||||
|
||||
@@ -56,7 +56,7 @@ How the text system works, for the entries below: `CPTextView` does not use CSS
|
||||
|
||||
- Fonts through `CPFontManager` and `CPFontPanel`.
|
||||
- RTF reading and writing.
|
||||
- Images and lists in text.
|
||||
- Images in text.
|
||||
- A storage facade, with saving and opening in IndexedDB and through HTTP.
|
||||
- An asynchronous fetch test: fetch the text of a page and show it for editing. The page must allow cross-origin requests.
|
||||
- An inspector pane with `CPSplitViewController` and `CPViewController`, with a View menu item that sends `toggleInspector:`.
|
||||
@@ -83,4 +83,7 @@ How the text system works, for the entries below: `CPTextView` does not use CSS
|
||||
- Format > Font > Baseline: superscript, subscript, raised and lowered baselines lay out correctly, and Use Default restores the text, for an exact or a wider selection.
|
||||
- Format > Text > Justify: wrapped lines fill the width between the indents, with the spare width spread over the spaces; the last line of a paragraph stays left aligned. Centred text is centred between the indents, and spaces at the end of a wrapped line hang outside the alignment.
|
||||
- Format > Font > Copy Style and Paste Style: the style of the first character of the selection is applied to the whole target selection, which keeps its paragraph style.
|
||||
- Format > List: bulleted and numbered lists, applied to one or several paragraphs and removed with None. Return starts the next item and keeps the item's text style; Return in an empty item ends the list. Tab at the start of an item nests it, and Shift-Tab returns it. Delete at the start of an item's text removes it from the list. The items are numbered again after each change.
|
||||
- Format > Text > Copy Ruler and Paste Ruler with lists: pasting adds or removes list markers, on one or several paragraphs, and a pasted item joins the neighbouring list.
|
||||
- Typing at the end of styled text, and the font panel, use the style of the character before the insertion point.
|
||||
- Builds with both the legacy toolchain and capp-build.
|
||||
Reference in new issue
Block a user