AppKit: Add magnification support to CPScrollView and zoom controls to TextEditor

CPScrollView previously lacked magnification support, preventing document
zooming and leaving view scaling infrastructure without practical test coverage.
Additionally, views resetting to a 1.0 scale retained stale CSS transform properties,
and legacy cibs compiled via nib2cib omitted explicit magnification limits,
breaking zoom functionality in older archives.

* AppKit/CPScrollView.j:
  - Implement magnification methods and properties (`magnification`, `setMagnification:`, `setMagnification:centeredAtPoint:`, `allowsMagnification`, `minMagnification`, and `maxMagnification`) modelled on NSScrollView, with keyed archiving.
  - Scale clip view bounds and synchronize resizing document view widths.
  - Define standard Cocoa default limits (0.25 and 4.0) as named constants.
  - Implement robust decoding validation to assign default limits to unset or invalid archive values and reject non-positive magnification inputs, ensuring full compatibility with legacy cibs.

* AppKit/CPView.j:
  - Remove stale CSS scale styling when a view's scale factor returns to 1.0.

* Tests/AppKit/CPScrollViewTest.j:
  - Add comprehensive unit tests covering magnification scaling, archiving, and fallback decoding for unconfigured limits.

* Tests/Manual/TextEditor/DocumentWindowController.j:
  - Implement zoom action handlers (`zoomToLevel:`, `zoomToActualSize:`, `zoomIn:`, and `zoomOut:`) driven by menu item tag values.

* Tests/Manual/TextEditor/Resources/MainMenu.xib, MainMenu.cib:
  - Add View menu options for Actual Size, Zoom In, Zoom Out, and explicit Zoom Levels (25% to 200%).

* Tests/Manual/TextEditor/WORKLIST.md:
  - Mark zoom functionality as tested; log discovered scaling edge cases as open items.

CPScrollView and TextEditor now support robust document magnification from 25% to 200%, validating core view scaling mechanics.
This commit is contained in:
David Richardson committed 2026-10-24 17:57:21 -06:00
1 parent a7ac7fd3b1
commit dc6bebd7eb
7 files changed
+474 -7

No files matched your search

+171 -1
View File
@@ -101,6 +101,15 @@ var CPScrollViewWillStartLiveScrollNotification = @"CPScrollViewWillStartLiveScr
var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
CPScrollerStyleGlobalChangeNotification = @"CPScrollerStyleGlobalChangeNotification";
/*
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
@class CPScrollView
@@ -148,6 +157,11 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
CPRulerView _verticalRuler;
CPRulerView _horizontalRuler;
// Magnification Support
BOOL _allowsMagnification;
float _minMagnification;
float _maxMagnification;
}
@@ -318,6 +332,10 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
_hasHorizontalRuler = NO;
_rulersVisible = NO;
_allowsMagnification = NO;
_minMagnification = CPScrollViewDefaultMinMagnification;
_maxMagnification = CPScrollViewDefaultMaxMagnification;
_delegate = nil;
_scrollTimer = nil;
_implementedDelegateMethods = 0;
@@ -806,6 +824,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
@@ -1802,7 +1932,10 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
CPScrollViewHasHRulerKey = @"CPScrollViewHasHRuler",
CPScrollViewRulersVisibleKey = @"CPScrollViewRulersVisible",
CPScrollViewVRulerKey = @"CPScrollViewVRuler",
CPScrollViewHRulerKey = @"CPScrollViewHRuler";
CPScrollViewHRulerKey = @"CPScrollViewHRuler",
CPScrollViewAllowsMagnificationKey = @"CPScrollViewAllowsMagnification",
CPScrollViewMinMagnificationKey = @"CPScrollViewMinMagnification",
CPScrollViewMaxMagnificationKey = @"CPScrollViewMaxMagnification";
@implementation CPScrollView (CPCoding)
@@ -1837,6 +1970,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];
@@ -1903,6 +2056,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];
+13 -2
View File
@@ -2651,15 +2651,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]];
}
}
+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)];
@@ -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>
+10
View File
@@ -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.
@@ -92,4 +101,5 @@ How the text system works, for the entries below: `CPTextView` does not use CSS
- 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.