Merge branch 'main' into Aristo3

This commit is contained in:
daboe01 authored and GitHub committed 2026-10-25 16:11:36 +01:00
commit 05bc99690d
15 files changed
+1300 -29

No files matched your search

+170 -6
View File
@@ -101,11 +101,14 @@ var CPScrollViewWillStartLiveScrollNotification = @"CPScrollViewWillStartLiveScr
var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
CPScrollerStyleGlobalChangeNotification = @"CPScrollerStyleGlobalChangeNotification";
var CPScrollViewBorderSuffixes = @[@"no-border", @"line-border", @"bezel-border", @"groove-border"];
// _CPScrollViews will hold all created CPScrollView's in order to propagate changes
// of scroller global style
var _CPScrollViews;
/*
The default smallest and largest magnifications. These are not arbitrary: they are the
defaults that Apple documents for NSScrollView's minMagnification and maxMagnification, so a
scroll view behaves as in Cocoa unless its limits are set. They apply to a scroll view made in
code, and to one decoded from an archive that has no valid limits.
*/
var CPScrollViewDefaultMinMagnification = 0.25,
CPScrollViewDefaultMaxMagnification = 4.0;
/*!
@ingroup appkit
@@ -154,6 +157,11 @@ var _CPScrollViews;
CPRulerView _verticalRuler;
CPRulerView _horizontalRuler;
// Magnification Support
BOOL _allowsMagnification;
float _minMagnification;
float _maxMagnification;
}
@@ -353,6 +361,10 @@ var _CPScrollViews;
_hasHorizontalRuler = NO;
_rulersVisible = NO;
_allowsMagnification = NO;
_minMagnification = CPScrollViewDefaultMinMagnification;
_maxMagnification = CPScrollViewDefaultMaxMagnification;
_delegate = nil;
_scrollTimer = nil;
_implementedDelegateMethods = 0;
@@ -843,6 +855,118 @@ Notifies the delegate when the scroll view has finished scrolling.
}
// MARK: -
// MARK: Magnification
/*!
Returns \c YES if the user may change the magnification. The default is \c NO. As in Cocoa,
-setMagnification: and -setMagnification:centeredAtPoint: change it in code either way.
*/
- (BOOL)allowsMagnification
{
return _allowsMagnification;
}
/*!
Sets whether the user may change the magnification.
*/
- (void)setAllowsMagnification:(BOOL)shouldAllowMagnification
{
_allowsMagnification = !!shouldAllowMagnification;
}
/*!
Returns the smallest magnification. The default is 0.25, as in Cocoa.
*/
- (float)minMagnification
{
return _minMagnification;
}
/*!
Sets the smallest magnification.
*/
- (void)setMinMagnification:(float)aMagnification
{
_minMagnification = aMagnification;
}
/*!
Returns the largest magnification. The default is 4.0, as in Cocoa.
*/
- (float)maxMagnification
{
return _maxMagnification;
}
/*!
Sets the largest magnification.
*/
- (void)setMaxMagnification:(float)aMagnification
{
_maxMagnification = aMagnification;
}
/*!
Returns the magnification of the document view: 1.0 at actual size, 2.0 at twice the size.
*/
- (float)magnification
{
var boundsWidth = CGRectGetWidth([_contentView bounds]);
return boundsWidth ? CGRectGetWidth([_contentView frame]) / boundsWidth : 1.0;
}
/*!
Sets the magnification, keeping the centre of the visible part of the document in place.
@param aMagnification the magnification, limited to the minimum and maximum magnification
*/
- (void)setMagnification:(float)aMagnification
{
var visibleRect = [_contentView bounds];
[self setMagnification:aMagnification centeredAtPoint:CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect))];
}
/*!
Sets the magnification, and scrolls so that aPoint is at the centre of the visible area. As
in Cocoa, the clip view's bounds are scaled: at a magnification of 2.0 its bounds are half its
frame, so the document view is drawn at twice its size. A document view that resizes its width
with the clip view, as a text view that wraps to the window does, takes the new bounds width.
@param aMagnification the magnification, limited to the minimum and maximum magnification
@param aPoint the point in the clip view's bounds, which are the document view's coordinates,
to show at the centre
*/
- (void)setMagnification:(float)aMagnification centeredAtPoint:(CGPoint)aPoint
{
var magnification = MIN(MAX(aMagnification, _minMagnification), _maxMagnification),
frameSize = [_contentView frameSize];
/*
If the limits are not numbers, the clamp above gives NaN. The clip view's bounds would
then become NaN, which the browser ignores, and -magnification would still report 1.0,
so the zoom would silently do nothing. This happened with scroll views loaded from cibs
whose limits were never set; see -initWithCoder: and -encodeWithCoder:. A magnification
that is not a positive number is therefore ignored.
*/
if (!(magnification > 0.0) || !frameSize.width || !frameSize.height)
return;
var boundsSize = CGSizeMake(frameSize.width / magnification, frameSize.height / magnification),
documentView = [self documentView];
[_contentView setBoundsSize:boundsSize];
// The clip view applies its scale to its element when it is marked for display.
[_contentView setNeedsDisplay:YES];
if ([documentView autoresizingMask] & CPViewWidthSizable)
[documentView setFrameSize:CGSizeMake(boundsSize.width, [documentView frameSize].height)];
[_contentView scrollToPoint:CGPointMake(aPoint.x - boundsSize.width / 2.0, aPoint.y - boundsSize.height / 2.0)];
[self reflectScrolledClipView:_contentView];
}
// MARK: -
// MARK: Rulers
@@ -1866,7 +1990,10 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
CPScrollViewHasHRulerKey = @"CPScrollViewHasHRuler",
CPScrollViewRulersVisibleKey = @"CPScrollViewRulersVisible",
CPScrollViewVRulerKey = @"CPScrollViewVRuler",
CPScrollViewHRulerKey = @"CPScrollViewHRuler";
CPScrollViewHRulerKey = @"CPScrollViewHRuler",
CPScrollViewAllowsMagnificationKey = @"CPScrollViewAllowsMagnification",
CPScrollViewMinMagnificationKey = @"CPScrollViewMinMagnification",
CPScrollViewMaxMagnificationKey = @"CPScrollViewMaxMagnification";
@implementation CPScrollView (CPCoding)
@@ -1901,6 +2028,26 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
_cornerView = [aCoder decodeObjectForKey:CPScrollViewCornerViewKey];
_bottomCornerView = [aCoder decodeObjectForKey:CPScrollViewBottomCornerViewKey];
// Magnification decoding
_allowsMagnification = [aCoder decodeBoolForKey:CPScrollViewAllowsMagnificationKey];
_minMagnification = [aCoder decodeFloatForKey:CPScrollViewMinMagnificationKey];
_maxMagnification = [aCoder decodeFloatForKey:CPScrollViewMaxMagnificationKey];
/*
Cibs compiled by nib2cib can hold limits that are not numbers. nib2cib builds a
scroll view from a xib through NSScrollView's -NS_initWithCoder:, which does not run
-initWithFrame:, so the limits are never set; it then archives the scroll view with
-encodeWithCoder: below, which wrote the unset limits into the cib. nib2cib belongs
to the legacy toolchain, which is frozen, so the problem is handled here: a limit
that is missing or not a positive number gets the Cocoa default. Without this, a
scroll view loaded from such a cib could not change its magnification.
*/
if (!(_minMagnification > 0.0))
_minMagnification = CPScrollViewDefaultMinMagnification;
if (!(_maxMagnification > 0.0))
_maxMagnification = CPScrollViewDefaultMaxMagnification;
// Ruler decoding
_hasVerticalRuler = [aCoder decodeBoolForKey:CPScrollViewHasVRulerKey];
_hasHorizontalRuler = [aCoder decodeBoolForKey:CPScrollViewHasHRulerKey];
@@ -1964,6 +2111,23 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
[aCoder encodeInt:_scrollerStyle forKey:CPScrollViewScrollerStyleKey];
[aCoder encodeInt:_scrollerKnobStyle forKey:CPScrollViewScrollerKnobStyleKey];
// Magnification encoding
[aCoder encodeBool:_allowsMagnification forKey:CPScrollViewAllowsMagnificationKey];
/*
nib2cib archives scroll views with this method, after building them from a xib through
NSScrollView's -NS_initWithCoder:, which does not run -initWithFrame:. Their limits are
then unset, and writing them put values that are not numbers into the cib. nib2cib
belongs to the legacy toolchain, which is frozen, so the problem is handled here: only
limits that are positive numbers are written, and -initWithCoder: supplies the defaults
for the rest.
*/
if (_minMagnification > 0.0)
[aCoder encodeFloat:_minMagnification forKey:CPScrollViewMinMagnificationKey];
if (_maxMagnification > 0.0)
[aCoder encodeFloat:_maxMagnification forKey:CPScrollViewMaxMagnificationKey];
// Ruler encoding
[aCoder encodeBool:_hasVerticalRuler forKey:CPScrollViewHasVRulerKey];
[aCoder encodeBool:_hasHorizontalRuler forKey:CPScrollViewHasHRulerKey];
+11 -3
View File
@@ -1541,11 +1541,19 @@ var _objectsInRange = function(aList, aRange)
if (verticalOffset)
{
// An attachment view is moved through setFrameOrigin:, which also moves it on the
// screen. Changing its frame directly would leave it in place, because the next draw's
// setFrameOrigin: would then find it already at the new origin and do nothing.
if (_runs[i].view)
_runs[i].view._frame.origin.y += verticalOffset;
{
var viewOrigin = [_runs[i].view frameOrigin];
if (_runs[i].elem)
_runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px';
[_runs[i].view setFrameOrigin:CGPointMake(viewOrigin.x, viewOrigin.y + verticalOffset)];
}
// A span that has been placed moves with its line. One that has not is placed when drawn.
if (_runs[i].elem && _runs[i].elem.style.top)
_runs[i].elem.style.top = (parseFloat(_runs[i].elem.style.top) + verticalOffset) + 'px';
_runs[i].DOMpatched = YES;
}
+8 -4
View File
@@ -5045,16 +5045,20 @@ var _CPCopyPlaceholder = '-';
// Can we accept richtext? Then this is our preference (fixme: shift key to force plain text paste)
if ([currentFirstResponder respondsToSelector:@selector(isRichText)] && [currentFirstResponder isRichText])
{
var richtext = nativeClipboard.getData('text/rtf');
var richtext = nativeClipboard.getData('text/rtf'),
richData = [pasteboard stringForType:_CPASPboardType];
// RTF that this application wrote itself, on copy, describes the text on the internal
// pasteboard. The internal archive is pasted instead: it also keeps what RTF does not
// yet carry, such as images.
if (richtext && richData && richtext === [pasteboard stringForType:CPRTFPboardType])
richtext = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:richData]];
// prefer RTF form the outside of cappuccino
if (richtext)
else if (richtext)
richtext = [[_CPRTFParser new] parseRTF:richtext];
else
{
// If no RTF is available, try to get the internal represatation of richtext from the pasteboard
var richData = [pasteboard stringForType:_CPASPboardType];
if (richData)
richtext = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:richData]];
}
+10
View File
@@ -510,6 +510,16 @@ var _CPTypesetterDecimalSeparator = function()
if (viewSize.height > _lineHeight)
_lineHeight = viewSize.height - descent + leading;
// An attachment wider than the container stays on the line it overflows, so a break
// there keeps the line's height and baseline with the attachment's size applied.
// Otherwise the attachment would be placed on the baseline of the text before it,
// and drawn above its line.
if (CPMaxRange(wrapRange) === CPMaxRange(lineRange))
{
wrapRange._height = _lineHeight;
wrapRange._base = _lineBase;
}
ascent = viewSize.height;
break;
}
+353 -4
View File
@@ -25,6 +25,7 @@
@import "CPFontManager.j"
@import "CPParagraphStyle.j"
@import "_CPTableTextAttachment.j"
@import "CPTextList.j"
@global CPLeftTextAlignment
@global CPRightTextAlignment
@@ -57,6 +58,209 @@ var cp1252Map = {
0x9E: 0x017E, 0x9F: 0x0178
};
/*
The marker format names of the RTF number formats (\levelnfc). 23 is a bullet.
*/
var _CPRTFNumberFormatNames = {
0: "decimal",
1: "upper-roman",
2: "lower-roman",
3: "upper-alpha",
4: "lower-alpha"
},
_CPRTFBulletFormats = {
"\u2022": "{disc}",
"\u25E6": "{circle}",
"\u25AA": "{square}",
"\u2043": "{hyphen}",
"\u25C6": "{diamond}",
"\u2713": "{check}",
"\u25A1": "{box}"
};
/*
Returns the index just after the group that opens with the brace at anOpenIndex in rtf. Escaped
braces, \{ and \}, do not open or close groups.
*/
var _CPRTFGroupEnd = function(rtf, anOpenIndex)
{
var depth = 0;
for (var i = anOpenIndex; i < rtf.length; i++)
{
var character = rtf.charAt(i);
if (character === "\\")
i++;
else if (character === "{")
depth++;
else if (character === "}" && --depth === 0)
return i + 1;
}
return rtf.length;
};
/*
Returns the groups in rtf that open with aPattern, a regular expression that matches the
opening brace and the group's first control word.
*/
var _CPRTFGroups = function(rtf, aPattern)
{
var groups = [],
match;
aPattern.lastIndex = 0;
while ((match = aPattern.exec(rtf)))
{
var end = _CPRTFGroupEnd(rtf, match.index);
groups.push(rtf.substring(match.index, end));
aPattern.lastIndex = end;
}
return groups;
};
/*
Decodes the text of a list level, the content of its \leveltext destination after the length
byte, into parts: strings, and numbers for the levels whose item numbers take their place. A
\'hh escape with a value below 9 is a level number; any other is a character.
*/
var _CPRTFLevelTextParts = function(aLevelText)
{
var parts = [],
literal = "",
i = 0;
while (i < aLevelText.length)
{
var character = aLevelText.charAt(i);
if (character === ";")
break;
if (character !== "\\")
{
literal += character;
i++;
continue;
}
var hexMatch = /^\\'([0-9a-fA-F]{2})/.exec(aLevelText.substring(i)),
unicodeMatch = /^\\u(-?\d+) ?\??/.exec(aLevelText.substring(i)),
wordMatch = /^\\[a-z]+-?\d* ?/.exec(aLevelText.substring(i));
if (hexMatch)
{
var value = parseInt(hexMatch[1], 16);
if (value < 9)
{
if (literal)
parts.push(literal);
literal = "";
parts.push(value);
}
else
literal += String.fromCharCode(value);
i += hexMatch[0].length;
}
else if (unicodeMatch)
{
var code = parseInt(unicodeMatch[1], 10);
literal += String.fromCharCode(code < 0 ? code + 65536 : code);
i += unicodeMatch[0].length;
}
else if (wordMatch)
i += wordMatch[0].length;
else
{
literal += aLevelText.charAt(i + 1);
i += 2;
}
}
if (literal)
parts.push(literal);
return parts;
};
/*
Returns the definition of one list level, {format, options, start}, from its \listlevel group.
Cocoa writes the marker format in \levelmarker. For other writers, the format is made from the
number format and the level's text.
*/
var _CPRTFListLevelDefinition = function(aLevelGroup, aLevel)
{
var markerMatch = /\{\\\*\\levelmarker ?((?:\\.|[^\\}])*)\}/.exec(aLevelGroup),
startMatch = /\\levelstartat(-?\d+)/.exec(aLevelGroup),
numberFormatMatch = /\\levelnfc(\d+)/.exec(aLevelGroup),
levelTextMatch = /\{\\leveltext(?:\\leveltemplateid-?\d+)? ?\\'[0-9a-fA-F]{2}((?:\\.|[^\\}])*)\}/.exec(aLevelGroup),
parts = levelTextMatch ? _CPRTFLevelTextParts(levelTextMatch[1]) : [],
prepends = /\\levelprepend(?![a-z])/.test(aLevelGroup),
format = nil;
for (var i = 0; i < parts.length; i++)
if (typeof parts[i] === "number" && parts[i] < aLevel)
prepends = YES;
if (markerMatch)
format = markerMatch[1].replace(/\\([\\{}])/g, "$1");
else
{
var numberFormat = numberFormatMatch ? parseInt(numberFormatMatch[1], 10) : 0;
if (numberFormat === 23)
{
var bullet = parts.join("");
format = _CPRTFBulletFormats[bullet] || bullet || CPTextListMarkerDisc;
}
else
{
// The text around this level's own number. Text after an enclosing level's number
// ends that level's marker, which is prepended, so only text before any number is
// this level's prefix. Its suffix runs up to the next number.
var name = "{" + (_CPRTFNumberFormatNames[numberFormat] || "decimal") + "}",
prefix = "",
suffix = "",
found = NO,
seenEnclosingNumber = NO;
for (var i = 0; i < parts.length; i++)
{
if (parts[i] === aLevel)
found = YES;
else if (typeof parts[i] === "number")
{
if (found)
break;
seenEnclosingNumber = YES;
}
else if (found)
suffix += parts[i];
else if (!seenEnclosingNumber)
prefix += parts[i];
}
format = prefix + name + suffix;
}
}
return {
format: format,
options: prepends ? CPTextListPrependEnclosingMarker : 0,
start: startMatch ? parseInt(startMatch[1], 10) : 1
};
};
// Hold the attributes of the current run
@implementation _RTFAttribute : CPObject
{
@@ -76,6 +280,13 @@ var cp1252Map = {
CPTabStopType _nextTabType;
int superscript;
float baselineOffset;
// The first line indent, \fi, is an offset from the left indent, \li, in RTF.
float _firstLineOffset;
// The RTF list (\lsN) and level (\ilvlN) of a list item, or 0 and 0.
int _listNumber;
int _listLevel;
}
- (id)init
@@ -110,6 +321,9 @@ var cp1252Map = {
mynew._nextTabType = _nextTabType;
mynew.superscript = superscript;
mynew.baselineOffset = baselineOffset;
mynew._firstLineOffset = _firstLineOffset;
mynew._listNumber = _listNumber;
mynew._listLevel = _listLevel;
return mynew;
}
@@ -162,6 +376,9 @@ var cp1252Map = {
paragraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
_tabChanged = NO;
_nextTabType = CPLeftTabStopType;
_firstLineOffset = 0.0;
_listNumber = 0;
_listLevel = 0;
}
- (void)resetFont
@@ -342,6 +559,17 @@ var kRgsymRtf = {
CPMutableArray _tableRows;
CPMutableArray _currentRow;
CPString _currentCellText;
// Text lists: the level definitions of each RTF list by its \listid, the list of each
// override by its \ls number, and the text lists made for each override, one for each level.
Object _listDefinitions;
Object _listOverrides;
Object _textListsByOverride;
// Unicode: the number of fallback characters after each \uN (\ucN), and how many of them are
// still to be skipped.
int _unicodeFallbackCount;
int _fallbackCharactersToSkip;
}
- (id)init
@@ -367,6 +595,13 @@ var kRgsymRtf = {
_currentRow = nil;
_currentCellText = "";
_listDefinitions = {};
_listOverrides = {};
_textListsByOverride = {};
_unicodeFallbackCount = 1;
_fallbackCharactersToSkip = 0;
// Safe Initialization
_currentRun = [_RTFAttribute new];
}
@@ -834,16 +1069,51 @@ var kRgsymRtf = {
_currentRun.fontSize = parseInt(param) / 2;
break;
case "fi": // first line indent
case "fi": // first line indent, an offset from the left indent
if (_currentRun)
[_currentRun.paragraph setFirstLineHeadIndent:parseInt(param) / 20.0];
{
_currentRun._firstLineOffset = parseInt(param) / 20.0;
[_currentRun.paragraph setFirstLineHeadIndent:[_currentRun.paragraph headIndent] + _currentRun._firstLineOffset];
}
break;
case "li": // left indent / head indent
if (_currentRun)
{
[_currentRun.paragraph setHeadIndent:parseInt(param) / 20.0];
[_currentRun.paragraph setFirstLineHeadIndent:[_currentRun.paragraph headIndent] + _currentRun._firstLineOffset];
}
break;
case "ls": // the RTF list of a list item
if (_currentRun)
{
_currentRun._listNumber = parseInt(param);
[self _applyTextListsToCurrentRun];
}
break;
case "ilvl": // the level of a list item
if (_currentRun)
{
_currentRun._listLevel = parseInt(param);
[self _applyTextListsToCurrentRun];
}
break;
case "uc": // the number of fallback characters after each Unicode character
_unicodeFallbackCount = parseInt(param) || 0;
break;
case "u": // a Unicode character, followed by its fallback characters
var code = parseInt(param);
// Fallback characters are skipped only in text that is kept.
if (_curState === 0)
_fallbackCharactersToSkip = _unicodeFallbackCount;
return String.fromCharCode(code < 0 ? code + 65536 : code);
case "ri": // right indent / tail indent
if (_currentRun)
[_currentRun.paragraph setTailIndent:parseInt(param) / 20.0];
@@ -932,6 +1202,76 @@ var kRgsymRtf = {
return [self _translateKeyword:keyword parameter:param fParameter:fParam];
}
/*
Reads the list table and the list override table, which are destinations the parser otherwise
skips, before the text is parsed. Paragraphs then refer to the lists with \lsN and \ilvlN.
*/
- (void)_parseListTables:(CPString)rtf
{
var lists = _CPRTFGroups(rtf, /\{\\list(?![a-z])/g);
for (var i = 0; i < lists.length; i++)
{
// Only the list itself has a \listid; its levels have \leveltemplateid.
var idMatch = /\\listid(-?\d+)/.exec(lists[i]),
levels = _CPRTFGroups(lists[i], /\{\\listlevel(?![a-z])/g),
definitions = [];
if (!idMatch)
continue;
for (var level = 0; level < levels.length; level++)
definitions.push(_CPRTFListLevelDefinition(levels[level], level));
_listDefinitions[idMatch[1]] = definitions;
}
var overrides = _CPRTFGroups(rtf, /\{\\listoverride(?![a-z])/g);
for (var i = 0; i < overrides.length; i++)
{
var idMatch = /\\listid(-?\d+)/.exec(overrides[i]),
numberMatch = /\\ls(\d+)/.exec(overrides[i]);
if (idMatch && numberMatch)
_listOverrides[numberMatch[1]] = idMatch[1];
}
}
/*
Gives the current run's paragraph the text lists of its RTF list and level. The text lists of
an RTF list are made once, one for each level, so the items of a list share them and are
numbered together.
*/
- (void)_applyTextListsToCurrentRun
{
var listNumber = _currentRun._listNumber;
if (!listNumber)
{
[_currentRun.paragraph setTextLists:[]];
return;
}
var definitions = _listDefinitions[_listOverrides[listNumber]] || [],
textLists = _textListsByOverride[listNumber];
if (!textLists)
textLists = _textListsByOverride[listNumber] = [];
for (var level = 0; level <= _currentRun._listLevel; level++)
{
if (!textLists[level])
{
var definition = definitions[level] || {format: CPTextListMarkerDisc, options: 0, start: 1};
textLists[level] = [[CPTextList alloc] initWithMarkerFormat:definition.format options:definition.options startingItemNumber:definition.start];
}
}
[_currentRun.paragraph setTextLists:textLists.slice(0, _currentRun._listLevel + 1)];
}
- (void)_appendPlainString:(CPString) aString
{
if (_inTableActive)
@@ -953,6 +1293,8 @@ var kRgsymRtf = {
_currentParseIndex = -1;
[self _parseListTables:rtf];
var len = rtf.length,
tmp = '',
ch = '',
@@ -1035,7 +1377,9 @@ var kRgsymRtf = {
if (_hexreturn)
{
// Only append decoded characters if we are in the active body state
if (ch.length > 0 && _curState === 0)
if (ch.length > 0 && _curState === 0 && _fallbackCharactersToSkip > 0)
_fallbackCharactersToSkip--;
else if (ch.length > 0 && _curState === 0)
{
var byteVal = parseInt(ch, 16);
var unicodeVal = byteVal;
@@ -1065,7 +1409,12 @@ var kRgsymRtf = {
default:
lastchar = 0;
if (_curState == 0)
if (_curState == 0 && _fallbackCharactersToSkip > 0)
{
// A fallback character for the Unicode character before it.
_fallbackCharactersToSkip--;
}
else if (_curState == 0)
{
[self _appendPlainString:tmp];
}
+253 -3
View File
@@ -28,6 +28,7 @@
@import "CPGraphics.j"
@import "CPFontManager.j"
@import "_CPTableTextAttachment.j"
@import "CPTextList.j"
@global CPForegroundColorAttributeName
@global CPBackgroundColorAttributeName
@@ -57,6 +58,44 @@ var PAPERSIZE = @"PaperSize",
function _points2twips(a) { return (a) * 20.0; }
/*
Escapes text for RTF. Backslashes and braces are escaped, and every character outside ASCII
is written as a Unicode control word, \uN, with no fallback characters (\uc0), as Cocoa
writes it. Values above 32767 are written as negative numbers, as RTF requires.
*/
var _CPRTFEscapeUnicode = function(aString)
{
return aString.replace(/[^\x00-\x7f]/g, function(aCharacter)
{
var code = aCharacter.charCodeAt(0);
return "\\uc0\\u" + (code > 32767 ? code - 65536 : code) + " ";
});
};
/*
Returns aNumber as two hexadecimal digits, for the \'hh escapes of a list level's text.
*/
var _CPRTFHexByte = function(aNumber)
{
return ("0" + (aNumber & 0xff).toString(16)).slice(-2);
};
/*
The RTF number format (\levelnfc) of each marker format name. Bullets are 23. Hexadecimal and
octal have no RTF number format, so other readers show them as decimal numbers; Cocoa reads
the marker format itself from \levelmarker.
*/
var _CPRTFLevelNumberFormats = {
"decimal": 0,
"upper-roman": 1,
"lower-roman": 2,
"upper-alpha": 3,
"upper-latin": 3,
"lower-alpha": 4,
"lower-latin": 4
};
@implementation _CPRTFProducer : CPObject
{
CPAttributedString text;
@@ -68,6 +107,11 @@ function _points2twips(a) { return (a) * 20.0; }
CPColor fgColor;
CPColor bgColor;
CPColor ulColor;
// The outermost text lists in the text, each written as one RTF list, and for each, the text
// list of every level, as first met.
CPArray textLists;
CPArray textListLevels;
}
// MARK: -
@@ -94,6 +138,9 @@ function _points2twips(a) { return (a) * 20.0; }
fgColor = [CPColor blackColor];
bgColor= [CPColor whiteColor];
textLists = [];
textListLevels = [];
}
return self;
@@ -246,6 +293,7 @@ function _points2twips(a) { return (a) * 20.0; }
result = [CPString stringWithString:@"{\\rtf1\\ansi"];
result += [self fontTable];
result += [self colorTable];
result += [self listTables];
result += [self documentAttributes];
return result;
@@ -313,7 +361,8 @@ function _points2twips(a) { return (a) * 20.0; }
break;
}
var twips = _points2twips([paraStyle firstLineHeadIndent]);
// In RTF the first line indent is an offset from the left indent.
var twips = _points2twips([paraStyle firstLineHeadIndent] - [paraStyle headIndent]);
if (twips != 0.0)
headerString += [CPString stringWithFormat:@"\\fi%d", twips];
@@ -375,12 +424,165 @@ function _points2twips(a) { return (a) * 20.0; }
headerString += [CPString stringWithFormat:@"\\tx%d",_points2twips([tab location])];
}
// A list item names its RTF list and its level.
var lists = [paraStyle textLists];
if ([lists count])
headerString += [CPString stringWithFormat:@"\\ls%d\\ilvl%d", [self listNumberForTextLists:lists], [lists count] - 1];
return headerString;
}
// MARK: -
// MARK: Text lists
/*
Returns the number of the RTF list for a paragraph's text lists, and records the list of each
level. Each outermost text list becomes one RTF list, with one level for each nesting depth.
*/
- (int)listNumberForTextLists:(CPArray)someLists
{
var index = [textLists indexOfObjectIdenticalTo:someLists[0]];
if (index === CPNotFound)
{
index = textLists.length;
textLists.push(someLists[0]);
textListLevels.push([]);
}
var levels = textListLevels[index];
for (var level = 0; level < someLists.length; level++)
if (!levels[level])
levels[level] = someLists[level];
return index + 1;
}
/*
Returns the list table and the list override table for the text lists in the text. Each RTF
list is a hybrid list with one level for each nesting depth, and one override, \lsN, that
paragraphs refer to.
*/
- (CPString)listTables
{
if (!textLists.length)
return @"";
var listTable = @"{\\*\\listtable",
overrideTable = @"{\\*\\listoverridetable";
for (var i = 0; i < textLists.length; i++)
{
var listNumber = i + 1,
levels = textListLevels[i];
listTable += "{\\list\\listtemplateid" + listNumber + "\\listhybrid";
for (var level = 0; level < levels.length; level++)
listTable += [self listLevelString:levels atLevel:level templateID:listNumber * 100 + level + 1];
listTable += "{\\listname ;}\\listid" + listNumber + "}";
overrideTable += "{\\listoverride\\listid" + listNumber + "\\listoverridecount0\\ls" + listNumber + "}";
}
return listTable + "}\n" + overrideTable + "}\n";
}
/*
Returns the RTF definition of one list level. Cocoa reads the marker format from the
\levelmarker destination and the prepending of the enclosing marker from \levelprepend. Other
readers use the number format (\levelnfc), the starting number (\levelstartat) and the
level's text (\leveltext), in which \'0N stands for the number of level N.
*/
- (CPString)listLevelString:(CPArray)someLevels atLevel:(int)aLevel templateID:(int)aTemplateID
{
var list = someLevels[aLevel],
format = [list markerFormat],
match = format.match(/\{([a-z-]+)\}/),
name = match ? match[1] : "",
numberFormat = [list isOrdered] ? (_CPRTFLevelNumberFormats[name] || 0) : 23,
parts = [self levelTextParts:someLevels atLevel:aLevel],
levelText = "",
levelNumbers = "",
length = 0,
indent = 720 * (aLevel + 1);
for (var i = 0; i < parts.length; i++)
{
var part = parts[i];
if (typeof part === "number")
{
length++;
levelText += "\\'" + _CPRTFHexByte(part);
levelNumbers += "\\'" + _CPRTFHexByte(length);
}
else
{
length += part.length;
levelText += _CPRTFEscapeUnicode(part.replace(/[\\{};]/g, function(aCharacter)
{
return "\\'" + _CPRTFHexByte(aCharacter.charCodeAt(0));
}));
}
}
return "{\\listlevel\\levelnfc" + numberFormat + "\\levelnfcn" + numberFormat +
"\\leveljc0\\leveljcn0\\levelfollow0\\levelstartat" + [list startingItemNumber] +
"\\levelspace360\\levelindent0{\\*\\levelmarker " + _CPRTFEscapeUnicode(format.replace(/([\\{}])/g, "\\$1")) + "}" +
(([list listOptions] & CPTextListPrependEnclosingMarker) ? "\\levelprepend" : "") +
"{\\leveltext\\leveltemplateid" + aTemplateID + "\\'" + _CPRTFHexByte(length) + levelText + ";}" +
"{\\levelnumbers" + levelNumbers + ";}\\fi-720\\li" + indent + "\\lin" + indent + " }";
}
/*
Returns the parts of a list level's text: strings, and the level numbers whose item numbers
take their place. A bullet level is its bullet. A numbered level is the text around its number,
after the enclosing level's text when the list prepends the enclosing marker.
*/
- (CPArray)levelTextParts:(CPArray)someLevels atLevel:(int)aLevel
{
var list = someLevels[aLevel],
format = [list markerFormat];
if (![list isOrdered])
return [[list markerForItemNumber:1]];
var match = format.match(/\{[a-z-]+\}/),
parts = [];
if (aLevel > 0 && ([list listOptions] & CPTextListPrependEnclosingMarker))
parts = [self levelTextParts:someLevels atLevel:aLevel - 1];
if (match.index > 0)
parts.push(format.substring(0, match.index));
parts.push(aLevel);
if (match.index + match[0].length < format.length)
parts.push(format.substring(match.index + match[0].length));
return parts;
}
- (CPString)runStringForString:(CPString) substring
attributes:(CPDictionary) attributes
paragraphStart:(BOOL) first
{
return [self runStringForString:substring attributes:attributes paragraphStart:first listMarker:NO];
}
/*
As -runStringForString:attributes:paragraphStart:. When isListMarker is YES, the run is the
marker at the start of a list item, and is written in a \listtext group, as Cocoa writes it.
Readers that understand lists number the items themselves; others show the marker text.
*/
- (CPString)runStringForString:(CPString) substring
attributes:(CPDictionary) attributes
paragraphStart:(BOOL) first
listMarker:(BOOL) isListMarker
{
var unwrap = function(obj) {
if (!obj) return null;
@@ -704,11 +906,18 @@ function _points2twips(a) { return (a) * 20.0; }
}
}
// An image attachment is not written yet, so its attachment character is left out.
substring = substring.replace(/\uFFFC/g, '');
substring = substring.replace(/\\/g, '\\\\');
substring = substring.replace(/\n/g, '\\par\n');
substring = substring.replace(/\t/g, '\\tab ');
substring = substring.replace(/{/g, '\\{');
substring = substring.replace(/}/g, '\\}');
substring = _CPRTFEscapeUnicode(substring);
if (isListMarker)
substring = "{\\listtext " + substring + "}";
if (!first)
{
@@ -752,13 +961,29 @@ function _points2twips(a) { return (a) * 20.0; }
substring,
runString;
attributes = [text attributesAtIndex:CPMaxRange(currRange)
var runStart = CPMaxRange(currRange);
attributes = [text attributesAtIndex:runStart
longestEffectiveRange:currRange
inRange:completeRange];
// The longest effective range can reach back before the run's start, after a list marker
// that shares its attributes was written as a run of its own.
if (currRange.location < runStart)
currRange = CPMakeRange(runStart, CPMaxRange(currRange) - runStart);
// A list item's marker, a tab, the marker and a tab, is written as a run of its own.
var markerLength = paragraphStart ? [self listMarkerLengthAtIndex:currRange.location attributes:attributes] : 0,
isListMarker = markerLength > 0 && markerLength <= currRange.length;
if (isListMarker)
currRange.length = markerLength;
substring = [string substringWithRange:currRange];
runString = [self runStringForString:substring
attributes:attributes
paragraphStart:paragraphStart];
paragraphStart:paragraphStart
listMarker:isListMarker];
result += runString;
@@ -772,6 +997,31 @@ function _points2twips(a) { return (a) * 20.0; }
}
/*
Returns the length of the marker at the start of a list item, a tab, the marker and a tab, or
0 when the paragraph is not a list item or does not start with a marker.
*/
- (unsigned)listMarkerLengthAtIndex:(unsigned)anIndex attributes:(CPDictionary)attributes
{
var string = [text string];
if (![[[attributes objectForKey:CPParagraphStyleAttributeName] textLists] count] || string.charAt(anIndex) !== "\t")
return 0;
for (var i = anIndex + 1; i < string.length; i++)
{
var character = string.charAt(i);
if (character === "\t")
return i - anIndex + 1;
if (character === "\n")
break;
}
return 0;
}
- (CPString)RTFDStringFromAttributedString:(CPAttributedString)aText
documentAttributes:(CPDictionary)dict
{
@@ -556,3 +556,39 @@
}
@end
var _CPTableTextAttachmentHeadersKey = @"_CPTableTextAttachmentHeaders",
_CPTableTextAttachmentRowsKey = @"_CPTableTextAttachmentRows",
_CPTableTextAttachmentWidthKey = @"_CPTableTextAttachmentWidth",
_CPTableTextAttachmentIsEditableKey = @"_CPTableTextAttachmentIsEditable",
_CPTableTextAttachmentAcceptsRichTextKey = @"_CPTableTextAttachmentAcceptsRichText";
@implementation _CPTableTextAttachment (CPCoding)
/*
Decodes the table from its headers, rows, width and settings, and builds its cells again.
The cells are not decoded as subviews; they are made from the decoded text.
*/
- (id)initWithCoder:(CPCoder)aCoder
{
return [self initWithHeaders:[aCoder decodeObjectForKey:_CPTableTextAttachmentHeadersKey]
rows:[aCoder decodeObjectForKey:_CPTableTextAttachmentRowsKey]
width:[aCoder decodeFloatForKey:_CPTableTextAttachmentWidthKey] || 500.0
isEditable:[aCoder decodeBoolForKey:_CPTableTextAttachmentIsEditableKey]
acceptsRichText:[aCoder decodeBoolForKey:_CPTableTextAttachmentAcceptsRichTextKey]];
}
/*
Encodes the table's current text, including edits made in its cells, its width and its
settings. The cell views are not encoded.
*/
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeObject:[self headers] forKey:_CPTableTextAttachmentHeadersKey];
[aCoder encodeObject:[self rows] forKey:_CPTableTextAttachmentRowsKey];
[aCoder encodeFloat:CGRectGetWidth([self frame]) forKey:_CPTableTextAttachmentWidthKey];
[aCoder encodeBool:_isEditable forKey:_CPTableTextAttachmentIsEditableKey];
[aCoder encodeBool:_acceptsRichText forKey:_CPTableTextAttachmentAcceptsRichTextKey];
}
@end
+13 -2
View File
@@ -2659,15 +2659,26 @@ setBoundsOrigin:
*/
- (void)_applyCSSScalingTranformations
{
if (CPDOMAvailable && _isScaled)
if (!CPDOMAvailable)
return;
var browserPropertyTransform = CPBrowserStyleProperty(@"transform");
if (_isScaled)
{
var scale = [self scaleSize],
browserPropertyTransform = CPBrowserStyleProperty(@"transform"),
browserPropertyTransformOrigin = CPBrowserStyleProperty(@"transformOrigin");
self._DOMElement.style[browserPropertyTransform] = 'scale(' + scale.width + ', ' + scale.height + ')';
self._DOMElement.style[browserPropertyTransformOrigin] = '0 0';
[self _setDisplayServerSetStyleSize:[self frameSize]];
}
else if (self._DOMElement.style[browserPropertyTransform])
{
// A view whose scale has returned to 1.0 drops the scale it was drawn with.
self._DOMElement.style[browserPropertyTransform] = '';
[self _setDisplayServerSetStyleSize:[self frameSize]];
}
}
+138
View File
@@ -0,0 +1,138 @@
@import <AppKit/CPApplication.j>
@import <AppKit/CPTextView.j>
@implementation CPRTFTest : 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];
}
- (CPAttributedString)roundTrip:(CPAttributedString)aString
{
var rtf = [_CPRTFProducer produceRTF:aString documentAttributes:@{}];
return [[_CPRTFParser new] parseRTF:rtf];
}
- (CPArray)textListsAtIndex:(unsigned)anIndex ofString:(CPAttributedString)aString
{
return [[aString attribute:CPParagraphStyleAttributeName atIndex:anIndex effectiveRange:nil] textLists];
}
// MARK: Unicode
- (void)testUnicodeIsEscapedAndRead
{
var text = @"Café — “quoted” •",
rtf = [_CPRTFProducer produceRTF:[[CPAttributedString alloc] initWithString:text] documentAttributes:@{}];
[self assertTrue:rtf.indexOf("\\u8212 ") !== -1 message:@"an em dash is written as \\u8212"];
[self assertTrue:/^[\x00-\x7f]*$/.test(rtf) message:@"the RTF holds only ASCII"];
[self assert:text equals:[[[_CPRTFParser new] parseRTF:rtf] string]];
}
- (void)testUnicodeFallbackCharactersAreSkipped
{
var parsed = [[_CPRTFParser new] parseRTF:@"{\\rtf1\\ansi\\uc1 a\\u8212?b \\uc0\\u8211 c}"];
[self assert:@"a—b –c" equals:[parsed string]];
}
// MARK: Indents
- (void)testFirstLineIndentIsRelativeToTheLeftIndent
{
var parsed = [[_CPRTFParser new] parseRTF:@"{\\rtf1\\ansi\\pard\\li720\\fi-720 text}"],
paragraphStyle = [parsed attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil];
[self assert:36.0 equals:[paragraphStyle headIndent]];
[self assert:0.0 equals:[paragraphStyle firstLineHeadIndent]];
var style = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
[style setHeadIndent:36.0];
[style setFirstLineHeadIndent:18.0];
var roundTripped = [self roundTrip:[[CPAttributedString alloc] initWithString:@"text" attributes:@{CPParagraphStyleAttributeName: style}]],
roundTrippedStyle = [roundTripped attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil];
[self assert:36.0 equals:[roundTrippedStyle headIndent]];
[self assert:18.0 equals:[roundTrippedStyle firstLineHeadIndent]];
}
// MARK: Lists
- (void)testListFromTextEdit
{
var rtf = @"{\\rtf1\\ansi\\ansicpg1252\\cocoartf2822\n" +
@"{\\fonttbl\\f0\\fswiss\\fcharset0 Helvetica;}\n" +
@"{\\colortbl;\\red255\\green255\\blue255;}\n" +
@"{\\*\\expandedcolortbl;;}\n" +
@"{\\*\\listtable{\\list\\listtemplateid1\\listhybrid{\\listlevel\\levelnfc23\\levelnfcn23\\leveljc0\\leveljcn0\\levelfollow0\\levelstartat1\\levelspace360\\levelindent0{\\*\\levelmarker \\{disc\\}}{\\leveltext\\leveltemplateid1\\'01\\uc0\\u8226 ;}{\\levelnumbers;}\\fi-360\\li720\\lin720 }{\\listname ;}\\listid1}}\n" +
@"{\\*\\listoverridetable{\\listoverride\\listid1\\listoverridecount0\\ls1}}\n" +
@"\\pard\\tx220\\tx720\\li720\\fi-720\\pardirnatural\\partightenfactor0\n" +
@"\\ls1\\ilvl0\n" +
@"\\f0\\fs24 \\cf0 {\\listtext\t\\uc0\\u8226 \t}one\\\n" +
@"{\\listtext\t\\uc0\\u8226 \t}two}",
parsed = [[_CPRTFParser new] parseRTF:rtf],
firstLists = [self textListsAtIndex:0 ofString:parsed],
secondLists = [self textListsAtIndex:[[parsed string] length] - 1 ofString:parsed];
[self assert:@"\t•\tone\n\t•\ttwo" equals:[parsed string]];
[self assert:1 equals:[firstLists count]];
[self assert:@"{disc}" equals:[firstLists[0] markerFormat]];
[self assert:firstLists[0] equals:secondLists[0] message:@"the items of one RTF list share one text list"];
[self assert:0.0 equals:[[parsed attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil] firstLineHeadIndent]];
}
- (void)testListFromLevelTextWithoutMarkerFormat
{
var rtf = @"{\\rtf1\\ansi{\\*\\listtable{\\list\\listtemplateid1{\\listlevel\\levelnfc4\\levelstartat3{\\leveltext\\'03(\\'00);}{\\levelnumbers\\'02;}}{\\listlevel\\levelnfc0{\\leveltext\\'03\\'00.\\'01;}{\\levelnumbers\\'01\\'03;}}\\listid7}}" +
@"{\\*\\listoverridetable{\\listoverride\\listid7\\listoverridecount0\\ls2}}" +
@"\\pard\\ls2\\ilvl1 item}",
lists = [self textListsAtIndex:0 ofString:[[_CPRTFParser new] parseRTF:rtf]];
[self assert:2 equals:[lists count]];
[self assert:@"({lower-alpha})" equals:[lists[0] markerFormat]];
[self assert:3 equals:[lists[0] startingItemNumber]];
[self assert:@"{decimal}" equals:[lists[1] markerFormat]];
[self assertTrue:([lists[1] listOptions] & CPTextListPrependEnclosingMarker) message:@"a level text with the enclosing number prepends it"];
}
- (void)testNestedListRoundTrip
{
[textView setString:@"one\ntwo\nthree"];
[textView setSelectedRange:CPMakeRange(0, 13)];
[textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:CPTextListPrependEnclosingMarker]];
[textView setSelectedRange:CPMakeRange([[textView string] rangeOfString:@"two"].location, 0)];
[textView insertTab:nil];
var original = [textView textStorage],
rtf = [_CPRTFProducer produceRTF:original documentAttributes:@{}],
parsed = [[_CPRTFParser new] parseRTF:rtf],
nestedIndex = [[parsed string] rangeOfString:@"two"].location,
nestedLists = [self textListsAtIndex:nestedIndex ofString:parsed];
[self assertTrue:rtf.indexOf("{\\listtext ") !== -1 message:@"markers are written in \\listtext groups"];
[self assertTrue:rtf.indexOf("\\levelprepend") !== -1];
[self assert:[original string] equals:[parsed string]];
[self assert:2 equals:[nestedLists count]];
[self assert:@"{decimal}." equals:[nestedLists[1] markerFormat]];
[self assertTrue:([nestedLists[1] listOptions] & CPTextListPrependEnclosingMarker)];
[self assert:nestedLists[0] equals:[self textListsAtIndex:0 ofString:parsed][0] message:@"the nested item stays in the outer list"];
[self assert:36.0 equals:[[parsed attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil] headIndent]];
}
@end
+58
View File
@@ -28,6 +28,64 @@
[[scrollView verticalScroller] layoutSubviews];
}
- (void)testMagnification
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)],
documentView = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 600.0)];
[documentView setAutoresizingMask:CPViewWidthSizable];
[scrollView setDocumentView:documentView];
[self assert:1.0 equals:[scrollView magnification]];
[scrollView setMagnification:2.0];
[self assert:2.0 equals:[scrollView magnification]];
[self assert:CGRectGetWidth([[scrollView contentView] bounds]) equals:CGRectGetWidth([documentView frame]) message:@"a document view that resizes its width takes the clip view's bounds width"];
[scrollView setMagnification:0.5];
[self assert:0.5 equals:[scrollView magnification]];
[scrollView setMagnification:10.0];
[self assert:[scrollView maxMagnification] equals:[scrollView magnification] message:@"the magnification is limited to the maximum"];
[scrollView setMagnification:1.0];
[self assert:1.0 equals:[scrollView magnification]];
[self assert:CGRectGetWidth([[scrollView contentView] frame]) equals:CGRectGetWidth([[scrollView contentView] bounds])];
}
- (void)testMagnificationArchiving
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)];
[scrollView setAllowsMagnification:YES];
[scrollView setMinMagnification:0.5];
[scrollView setMaxMagnification:3.0];
var decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:scrollView]];
[self assertTrue:[decoded allowsMagnification]];
[self assert:0.5 equals:[decoded minMagnification]];
[self assert:3.0 equals:[decoded maxMagnification]];
}
- (void)testMagnificationDecodingWithoutValidLimits
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)];
// An archive made from a scroll view whose limits were never set, as nib2cib made before it
// set them, holds values that are not numbers.
scrollView._minMagnification = undefined;
scrollView._maxMagnification = undefined;
var decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:scrollView]];
[self assert:0.25 equals:[decoded minMagnification]];
[self assert:4.0 equals:[decoded maxMagnification]];
[decoded setMagnification:0.5];
[self assert:0.5 equals:[decoded magnification]];
}
- (void)testBothScrollersVisible
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
+11
View File
@@ -115,4 +115,15 @@
[self assert:[attachment _attachmentView] equals:[[textView textStorage] attribute:_CPAttachmentView atIndex:0 effectiveRange:nil]];
}
- (void)testTableAttachmentArchiving
{
var table = [[_CPTableTextAttachment alloc] initWithHeaders:[@"Name", @"Value"] rows:[[@"one", @"1"], [@"two", @"2"]] width:300.0],
decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:table]];
[self assertTrue:[decoded isKindOfClass:[_CPTableTextAttachment class]]];
[self assert:@"Value" equals:[[[decoded headers] objectAtIndex:1] string]];
[self assert:@"two" equals:[[[[decoded rows] objectAtIndex:1] objectAtIndex:0] string]];
[self assert:CGRectGetWidth([table frame]) equals:CGRectGetWidth([decoded frame]) message:@"the table keeps its width"];
}
@end
@@ -42,6 +42,15 @@ var DocumentWindowMinimumWidth = 360.0,
DocumentWindowMinimumHeight = 240.0;
/*
Orders two zoom levels, smallest first, for sorting.
*/
function compareZoomLevels(aLevel, anotherLevel)
{
return aLevel - anotherLevel;
}
/*!
Owns DocumentWindow.cib. The window's delegate outlet is connected to
File's Owner in DocumentWindow.xib, so this controller is the window's
@@ -125,6 +134,120 @@ var DocumentWindowMinimumWidth = 360.0,
[textView insertText:[TableExample attributedString]];
}
/*!
Returns the scroll view that encloses the text view.
The zoom commands do not change the text view itself. They change the
magnification of its scroll view, as TextEdit does: the scroll view
scales its clip view, which draws the text view, with everything inside
it, larger or smaller. The text wraps to the window at every
magnification, because the text view takes the width of the clip view.
*/
- (CPScrollView)documentScrollView
{
return [textView enclosingScrollView];
}
/*!
Sets the magnification to the zoom level in the sender's tag, a
percentage. Each item of View > Zoom Level sends this action, with its
level as its tag, for example 75 for 75%.
*/
- (@action)zoomToLevel:(id)aSender
{
[[self documentScrollView] setMagnification:[aSender tag] / 100.0];
}
/*!
Returns the text to actual size, a magnification of 1.0.
*/
- (@action)zoomToActualSize:(id)aSender
{
[[self documentScrollView] setMagnification:1.0];
}
/*!
Moves to the next zoom level above the current magnification.
*/
- (@action)zoomIn:(id)aSender
{
var level = [self zoomLevelFrom:[[self documentScrollView] magnification] direction:1];
if (level)
[[self documentScrollView] setMagnification:level];
}
/*!
Moves to the next zoom level below the current magnification.
*/
- (@action)zoomOut:(id)aSender
{
var level = [self zoomLevelFrom:[[self documentScrollView] magnification] direction:-1];
if (level)
[[self documentScrollView] setMagnification:level];
}
/*!
Returns the zoom levels, as magnifications in ascending order.
The menu items of View > Zoom Level are the only definition of the
levels: each item that sends zoomToLevel: carries its level as a
percentage in its tag. Zoom In and Zoom Out step through the same items,
so adding, removing or changing an item in MainMenu.xib changes all
three commands together.
*/
- (CPArray)zoomLevels
{
var levels = [];
[self collectZoomLevelsFromMenu:[CPApp mainMenu] into:levels];
return levels.sort(compareZoomLevels);
}
/*!
Adds to someLevels the level of every item in aMenu, and in its
submenus, that sends zoomToLevel:.
*/
- (void)collectZoomLevelsFromMenu:(CPMenu)aMenu into:(CPArray)someLevels
{
var items = [aMenu itemArray],
count = [items count];
for (var i = 0; i < count; i++)
{
var item = items[i];
if ([item action] === @selector(zoomToLevel:))
someLevels.push([item tag] / 100.0);
if ([item hasSubmenu])
[self collectZoomLevelsFromMenu:[item submenu] into:someLevels];
}
}
/*!
Returns the next zoom level above aMagnification, with a direction of 1,
or below it, with a direction of -1. Returns nil when there is none. The
comparison allows for rounding in the magnification.
*/
- (float)zoomLevelFrom:(float)aMagnification direction:(int)aDirection
{
var levels = [self zoomLevels],
count = levels.length;
for (var i = 0; i < count; i++)
{
var level = levels[aDirection > 0 ? i : count - 1 - i];
if (aDirection > 0 ? level > aMagnification + 0.001 : level < aMagnification - 0.001)
return level;
}
return nil;
}
/*!
Keeps the window at or above its minimum size while the user resizes it.
@@ -179,14 +302,35 @@ var DocumentWindowMinimumWidth = 360.0,
AppKit calls this method before it shows a menu, for each item whose
action reaches this controller through the responder chain. For Allow
Undo, it sets the item's check mark from the active document's text
view, so the mark follows the document whose window is in front. Every
item is enabled.
view, so the mark follows the document whose window is in front. For the
zoom commands, it checks the current zoom level and disables commands
that would not change the magnification. Every other item is enabled.
*/
- (BOOL)validateMenuItem:(CPMenuItem)aMenuItem
{
if ([aMenuItem action] === @selector(toggleAllowsUndo:))
var action = [aMenuItem action],
magnification = [[self documentScrollView] magnification];
if (action === @selector(toggleAllowsUndo:))
[aMenuItem setState:[textView allowsUndo] ? CPOnState : CPOffState];
/*
The zoom level that matches the current magnification is checked.
Zoom In and Zoom Out are disabled at the highest and lowest levels,
and Actual Size at actual size.
*/
if (action === @selector(zoomToLevel:))
[aMenuItem setState:ABS(magnification - [aMenuItem tag] / 100.0) < 0.001 ? CPOnState : CPOffState];
if (action === @selector(zoomIn:))
return !![self zoomLevelFrom:magnification direction:1];
if (action === @selector(zoomOut:))
return !![self zoomLevelFrom:magnification direction:-1];
if (action === @selector(zoomToActualSize:))
return ABS(magnification - 1.0) >= 0.001;
return YES;
}
@@ -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;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;D;K;6;CP$UIDd;2;96E;D;K;6;CP$UIDd;2;97E;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;98E;D;K;6;CP$UIDd;3;100E;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;KLine 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;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;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;3;109E;D;K;6;CP$UIDd;3;111E;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;K;Line truncated
@@ -597,6 +597,80 @@
<action selector="runToolbarCustomizationPalette:" target="-1" id="365"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="1rm-Jd-YMr"/>
<menuItem title="Actual Size" keyEquivalent="0" id="jhH-NV-2Ab">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="zoomToActualSize:" target="-1" id="VO9-re-O0i"/>
</connections>
</menuItem>
<menuItem title="Zoom In" keyEquivalent="." id="Soa-tt-vUl">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="zoomIn:" target="-1" id="cyL-Ic-hJD"/>
</connections>
</menuItem>
<menuItem title="Zoom Out" keyEquivalent="," id="nOn-Xu-5A8">
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
<connections>
<action selector="zoomOut:" target="-1" id="V1I-2e-rSt"/>
</connections>
</menuItem>
<menuItem title="Zoom Level" id="nmn-2j-0ly">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Zoom Level" id="Rh2-Z6-s8v">
<items>
<menuItem title="25%" tag="25" id="Qbl-nV-eU5">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="zoomToLevel:" target="-1" id="ylk-WR-FwV"/>
</connections>
</menuItem>
<menuItem title="50%" tag="50" id="QD5-wT-lnV">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="zoomToLevel:" target="-1" id="lgW-iJ-Xxg"/>
</connections>
</menuItem>
<menuItem title="75%" tag="75" id="nWv-Bb-jZ9">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="zoomToLevel:" target="-1" id="RPR-B7-7W1"/>
</connections>
</menuItem>
<menuItem title="100%" tag="100" id="KwK-D7-kuE">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="zoomToLevel:" target="-1" id="H7q-qa-UGn"/>
</connections>
</menuItem>
<menuItem title="125%" tag="125" id="qR5-be-XPV">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="zoomToLevel:" target="-1" id="eFM-ep-yYw"/>
</connections>
</menuItem>
<menuItem title="150%" tag="150" id="t4r-9t-fgU">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="zoomToLevel:" target="-1" id="raR-xi-9bV"/>
</connections>
</menuItem>
<menuItem title="175%" tag="175" id="v2C-Sa-xkV">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="zoomToLevel:" target="-1" id="vJp-E3-hdZ"/>
</connections>
</menuItem>
<menuItem title="200%" tag="200" id="tB2-Nv-dii">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="zoomToLevel:" target="-1" id="cra-IB-AC3"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
+17 -3
View File
@@ -15,9 +15,9 @@ TextEditor is a document-based test application and a tutorial. When the app use
### Text features not yet supported
- **Images in text are not fully tested or complete.** `CPTextAttachment` holds an image as one attachment character, and Edit > Insert Image… and Command-V with an image on the clipboard insert images. Not yet tested: several images inserted at once, copying, pasting and undoing an image within the document, and pasting a screenshot. Not yet implemented: dropping image files on the text view, which needs file access in the drag pasteboard (`_CPDOMDataTransferPasteboard`); pasting images with Edit > Paste from the menu, which reads plain text only; and RTF `\pict` and Markdown `![]()`, so an image is lost when text is copied to another application as RTF.
- **Tables can only be inserted from Markdown, and are not yet tested interactively.** The only source of tables is CPMarkdownParser, which builds the private `_CPTableTextAttachment`, a view in the text whose cells are text views of their own. Edit > Insert Table Example inserts a sample through it. Not yet tested: editing in a cell and what Tab does there, deleting the table and undoing, copying and pasting it within the document, narrowing the window, and Format menu commands while a cell is being edited. Known from the code: the table has no archiving, so copying it within the application may fail; nothing calls its `resizeToWidth:`, so it keeps its width of 500 points; and in the sample its right edge and the last row's bottom border are not drawn. A command to insert a table, with a fixed default size and no options, waits on these results.
- **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.
- **Images in text are not fully tested or complete.** `CPTextAttachment` holds an image as one attachment character, and Edit > Insert Image… and Command-V with an image on the clipboard insert images. Not yet tested: several images inserted at once, undoing an image change, and pasting a screenshot. Not yet implemented: dropping image files on the text view, which needs file access in the drag pasteboard (`_CPDOMDataTransferPasteboard`); pasting images with Edit > Paste from the menu, which reads plain text only; and RTF `\pict` and Markdown `![]()`, so an image is lost when text is copied to another application as RTF.
- **Tables can only be inserted from Markdown, and are not yet tested interactively.** The only source of tables is CPMarkdownParser, which builds the private `_CPTableTextAttachment`, a view in the text whose cells are text views of their own. Edit > Insert Table Example inserts a sample through it. Not yet tested: what Tab does in a cell, deleting the table and undoing, narrowing the window, and Format menu commands while a cell is being edited. Known from the code: nothing calls its `resizeToWidth:`, so it keeps its width of 500 points; and in the sample its right edge and the last row's bottom border are not drawn. A command to insert a table, with a fixed default size and no options, waits on these results.
- **Rich text does not reach native applications.** The RTF producer and parser now handle lists (`\listtable`, `\listoverridetable`, `\ls`, `\ilvl`, and Cocoa's `\levelmarker`), Unicode (`\uN`) and the first line indent as an offset from the left indent, and the unit tests round-trip them. But browsers do not put `text/rtf` from a page's clipboard event on the system clipboard in a form native applications read: text copied from TextEditor with Command-C reaches native TextEdit as plain text. RTF on the clipboard works only between Cappuccino applications in the same browser. Rich text for native applications needs an HTML producer (`text/html`) for the subset the text view makes. Not yet tested: what arrives from native TextEdit with Command-V, in each browser. RTF `\pict` is not implemented. CPMarkdownParser also writes "- item" as the literal text " • item", with no list.
### Text view
@@ -30,6 +30,15 @@ How the text system works, for the entries below: `CPTextView` does not use CSS
- **Marker appearance and vertical alignment differ from TextEdit.** The marker images, and where markers sit across the ruler, are interim until Aristo3 supplies its own; adjust them to match TextEdit then. A tab stop dragged up off the ruler follows the pointer and is clipped by the ruler's edge, as in TextEdit. Dragged down off the ruler, it stays on its line and is drawn faded until release; TextEdit's behaviour in that direction has not been checked.
### Zoom and view scaling
View > Zoom Level, Zoom In, Zoom Out and Actual Size set the magnification of the document's scroll view, which scales its clip view's bounds. Zoom exercises CPView's scaling through the whole view hierarchy, so it is the test for scaling defects, below and above 100%. Found in the first test, under Aristo2:
- **The ruler ignores the magnification.** Its tab stop markers follow the scaled text, but its scale and labels stay at 100%, so markers and units disagree. In Cocoa the ruler scales its units with the scroll view's magnification.
- **Autoresizing uses frame sizes, not bounds sizes.** `-[CPView resizeWithOldSuperviewSize:]` resizes a subview by the change in its superview's frame size, but a subview lives in its superview's bounds coordinates. Cocoa uses the bounds size. Under magnification, an autoresized subview changes by the wrong amount: above 100%, the text became wider than the window after it was resized.
- **Below 100%, a few points are cut off at the left.** The first letter of some lines is missing.
- **Below 100%, a grey band shows below the text.** The text view is only as tall as its text, and the clip view's background shows below it. Cocoa draws the document's background there.
### nib2cib
- **nib2cib ignores the ruler settings.** The "Uses Ruler" setting in Interface Builder does not reach the cib. `CPTextView` and `CPRulerView` also need to archive their ruler settings.
@@ -88,4 +97,9 @@ How the text system works, for the entries below: `CPTextView` does not use CSS
- A wide image fits the text width, and scales with the window without growing beyond its natural size. At the end of the text it is not clipped, and the scroll bars follow it.
- Format > Font > Bigger and Smaller (Command-+ and Command--) without a selection change the size of the text typed next, step by step, also in a blank document.
- Edit > Insert Table Example: the sample table appears between the surrounding sentences, with its header row and its bold and italic cells.
- Copy and paste within TextEditor with Command-C and Command-V, in one document and between documents: images, tables, including text edited in their cells, and lists are kept.
- Table cells can be edited, as a first pass.
- Pasting text with several images and a table: the table appears in place at once, and tables, images and text below a change move with their lines.
- A table wider than the text stays on its own line below the text before it when the window is narrowed, and is clipped at the right edge, without a horizontal scroll bar, as in Cocoa when text wraps to the window.
- View > Zoom Level, Zoom In, Zoom Out and Actual Size change the magnification below and above 100%, in the source app and in the build product; text, tables and images stay together at every level.
- Builds with both the legacy toolchain and capp-build.