mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-11-03 05:39:45 +00:00
Merge pull request #3351 from cappuccino/main-testing
Protect canonical AppKit and Foundation APIs with compatibility tests Add regression and drop-in compatibility test suites to preserve canonical AppKit and Foundation public contracts and archived behavior, incorporating immediate fixes for defects exposed by the improved tests.
This commit is contained in:
48 files changed
+16269
-129
No files matched your search
@@ -414,3 +414,85 @@ var secondItem = [[CPAccordionViewItem alloc] initWithIdentifier:@"secondSection
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
var CPAccordionViewItemIdentifierKey = @"CPAccordionViewItemIdentifierKey",
|
||||
CPAccordionViewItemViewKey = @"CPAccordionViewItemViewKey",
|
||||
CPAccordionViewItemLabelKey = @"CPAccordionViewItemLabelKey";
|
||||
|
||||
@implementation CPAccordionViewItem (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_identifier = [aCoder decodeObjectForKey:CPAccordionViewItemIdentifierKey];
|
||||
_view = [aCoder decodeObjectForKey:CPAccordionViewItemViewKey];
|
||||
_label = [aCoder decodeObjectForKey:CPAccordionViewItemLabelKey];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeObject:_identifier forKey:CPAccordionViewItemIdentifierKey];
|
||||
[aCoder encodeObject:_view forKey:CPAccordionViewItemViewKey];
|
||||
[aCoder encodeObject:_label forKey:CPAccordionViewItemLabelKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPAccordionViewItemsKey = @"CPAccordionViewItemsKey",
|
||||
CPAccordionViewItemHeaderPrototypeKey = @"CPAccordionViewItemHeaderPrototypeKey",
|
||||
CPAccordionViewExpandedItemIndexesKey = @"CPAccordionViewExpandedItemIndexesKey";
|
||||
|
||||
@implementation CPAccordionView (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [super initWithCoder:aCoder];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_items = [];
|
||||
_itemViews = [];
|
||||
_expandedItemIndexes = [CPIndexSet indexSet];
|
||||
|
||||
[self setItemHeaderPrototype:[aCoder decodeObjectForKey:CPAccordionViewItemHeaderPrototypeKey]];
|
||||
|
||||
var items = [aCoder decodeObjectForKey:CPAccordionViewItemsKey],
|
||||
expandedItemIndexes = [aCoder decodeObjectForKey:CPAccordionViewExpandedItemIndexesKey];
|
||||
|
||||
items.forEach(function(anItem)
|
||||
{
|
||||
[self addItem:anItem];
|
||||
});
|
||||
|
||||
items.forEach(function(anItem, anIndex)
|
||||
{
|
||||
if (![expandedItemIndexes containsIndex:anIndex])
|
||||
[self collapseItemAtIndex:anIndex];
|
||||
});
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
// The item views are recreated on decode from the items, so they are not encoded.
|
||||
var subviews = [self subviews];
|
||||
|
||||
[_itemViews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||
[super encodeWithCoder:aCoder];
|
||||
[self setSubviews:subviews];
|
||||
|
||||
[aCoder encodeObject:_items forKey:CPAccordionViewItemsKey];
|
||||
[aCoder encodeObject:_itemHeaderPrototype forKey:CPAccordionViewItemHeaderPrototypeKey];
|
||||
[aCoder encodeObject:_expandedItemIndexes forKey:CPAccordionViewExpandedItemIndexesKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -125,6 +125,11 @@ CPThemeStateAppearanceVibrantDark = CPThemeState("appearance-vibrant-dark")
|
||||
// MARK: -
|
||||
// MARK: Implementation
|
||||
|
||||
- (CPString)name
|
||||
{
|
||||
return _name;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)anObject
|
||||
{
|
||||
if (![anObject isKindOfClass:CPAppearance])
|
||||
|
||||
+12
-5
@@ -869,6 +869,11 @@ var CPBoxTypeKey = @"CPBoxTypeKey",
|
||||
// We're coming from elsewhere
|
||||
|
||||
_titleView = [aCoder decodeObjectForKey:CPBoxTitleViewKey];
|
||||
|
||||
// The archiver encodes these views after encodeWithCoder: returns, when they
|
||||
// are subviews again. They are added back below.
|
||||
_boxView._superview = nil;
|
||||
_titleView._superview = nil;
|
||||
}
|
||||
|
||||
_contentView = [aCoder decodeObjectForKey:CPBoxContentViewKey];
|
||||
@@ -877,7 +882,9 @@ var CPBoxTypeKey = @"CPBoxTypeKey",
|
||||
// FIXME: (see encodeWithCoder to understand why this is not possible)
|
||||
// FIXME: we fix this by hand. This is horrible so please find a structural solution !
|
||||
|
||||
if (_contentView)
|
||||
var contentViewNeedsBoxView = _contentView && [_contentView superview] !== _boxView;
|
||||
|
||||
if (contentViewNeedsBoxView)
|
||||
_contentView._superview = nil;
|
||||
|
||||
[_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
@@ -885,7 +892,7 @@ var CPBoxTypeKey = @"CPBoxTypeKey",
|
||||
[_boxView setAutoresizesSubviews:YES];
|
||||
[self setAutoresizesSubviews:YES];
|
||||
|
||||
if (_contentView)
|
||||
if (contentViewNeedsBoxView)
|
||||
[_boxView setSubviews:@[_contentView]];
|
||||
|
||||
[self addSubview:_boxView];
|
||||
@@ -945,9 +952,9 @@ var CPBoxTypeKey = @"CPBoxTypeKey",
|
||||
[aCoder encodeInt:_borderType forKey:CPBoxBorderTypeKey];
|
||||
[aCoder encodeObject:_title forKey:CPBoxTitleKey];
|
||||
[aCoder encodeInt:_titlePosition forKey:CPBoxTitlePositionKey];
|
||||
[aCoder encodeConditionalObject:_contentView forKey:CPBoxContentViewKey];
|
||||
[aCoder encodeConditionalObject:_titleView forKey:CPBoxTitleViewKey];
|
||||
[aCoder encodeConditionalObject:_boxView forKey:CPBoxBoxViewKey];
|
||||
[aCoder encodeObject:_contentView forKey:CPBoxContentViewKey];
|
||||
[aCoder encodeObject:_titleView forKey:CPBoxTitleViewKey];
|
||||
[aCoder encodeObject:_boxView forKey:CPBoxBoxViewKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -202,6 +202,8 @@ CPButtonImageOffset = 3.0;
|
||||
[self setBezelStyle:CPRoundRectBezelStyle];
|
||||
[self setBordered:YES];
|
||||
|
||||
_value = CPOffState;
|
||||
|
||||
[self _init];
|
||||
}
|
||||
|
||||
|
||||
@@ -390,6 +390,11 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey",
|
||||
_rightButtons = [aCoder decodeObjectForKey:CPButtonBarRightButtonsKey] || [];
|
||||
_hasResizeControl = [aCoder decodeBoolForKey:CPButtonBarHasResizeControlKey];
|
||||
_resizeControlIsLeftAligned = [aCoder decodeBoolForKey:CPButtonBarResizeControlIsLeftAlignedKey];
|
||||
|
||||
var buttons = [_buttons arrayByAddingObjectsFromArray:_rightButtons];
|
||||
|
||||
for (var i = [buttons count] - 1; i >= 0; i--)
|
||||
[buttons[i] addObserver:self forKeyPath:@"hidden" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
||||
+11
-4
@@ -1581,7 +1581,9 @@ function SHOULD_SELECT_ITEM(anOutlineView, anItem)
|
||||
_retargedChildIndex = theIndex;
|
||||
_shouldRetargetChildIndex = YES;
|
||||
|
||||
// set CPTableView's _retargetedDropRow based on retargetedItem and retargetedChildIndex
|
||||
// set CPTableView's _retargetedDropOperation and _retargetedDropRow based on retargetedItem and retargetedChildIndex
|
||||
_retargetedDropOperation = (_retargedChildIndex === CPOutlineViewDropOnItemIndex) ? CPTableViewDropOn : CPTableViewDropAbove;
|
||||
|
||||
var retargetedItemInfo = (_retargetedItem != nil) ? _itemInfosForItems[[_retargetedItem UID]] : _rootItemInfo;
|
||||
|
||||
if (_retargedChildIndex === [retargetedItemInfo.children count])
|
||||
@@ -2330,12 +2332,17 @@ var CPOutlineViewIndentationPerLevelKey = @"CPOutlineViewIndentationPerLevelKey"
|
||||
_outlineTableColumn = [aCoder decodeObjectForKey:CPOutlineViewOutlineTableColumnKey];
|
||||
_indentationPerLevel = [aCoder decodeFloatForKey:CPOutlineViewIndentationPerLevelKey];
|
||||
|
||||
_outlineViewDataSource = [aCoder decodeObjectForKey:CPOutlineViewDataSourceKey];
|
||||
_outlineViewDelegate = [aCoder decodeObjectForKey:CPOutlineViewDelegateKey];
|
||||
|
||||
[super setDataSource:self];
|
||||
[super setDelegate:self];
|
||||
|
||||
// The setters record which optional methods each object implements.
|
||||
var dataSource = [aCoder decodeObjectForKey:CPOutlineViewDataSourceKey];
|
||||
|
||||
if (dataSource)
|
||||
[self setDataSource:dataSource];
|
||||
|
||||
[self setDelegate:[aCoder decodeObjectForKey:CPOutlineViewDelegateKey]];
|
||||
|
||||
[self _updateIsViewBased];
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ CPPopUpButtonStatePullsDown = CPThemeState("pulls-down");
|
||||
|
||||
if (self)
|
||||
{
|
||||
// The value of a pop up button is its selected index, and not a button state.
|
||||
_value = nil;
|
||||
|
||||
[self selectItemAtIndex:CPNotFound];
|
||||
|
||||
_preferredEdge = CPMaxYEdge;
|
||||
|
||||
@@ -464,6 +464,12 @@ var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotificat
|
||||
return [super isOpaque] && [_cancelButton isOpaque] && [_searchButton isOpaque];
|
||||
}
|
||||
|
||||
- (void)setObjectValue:(id)anObject
|
||||
{
|
||||
[super setObjectValue:anObject];
|
||||
[self _updateCancelButtonVisibility];
|
||||
}
|
||||
|
||||
- (void)_updateCancelButtonVisibility
|
||||
{
|
||||
[_cancelButton setHidden:([[self stringValue] length] === 0)];
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
/*!
|
||||
Initializes a shadow with no offset or blur, and a black color at one third opacity.
|
||||
*/
|
||||
- (id)init
|
||||
{
|
||||
return [self _initWithOffset:CGSizeMakeZero() blurRadius:0.0 color:[CPColor colorWithWhite:0.0 alpha:1.0 / 3.0]];
|
||||
}
|
||||
|
||||
- (id)_initWithOffset:(CGSize)anOffset blurRadius:(float)aBlurRadius color:(CPColor)aColor
|
||||
{
|
||||
self = [super init];
|
||||
@@ -82,3 +90,25 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPShadowOffsetKey = @"CPShadowOffsetKey",
|
||||
CPShadowBlurRadiusKey = @"CPShadowBlurRadiusKey",
|
||||
CPShadowColorKey = @"CPShadowColorKey";
|
||||
|
||||
@implementation CPShadow (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
return [self _initWithOffset:[aCoder decodeSizeForKey:CPShadowOffsetKey]
|
||||
blurRadius:[aCoder decodeFloatForKey:CPShadowBlurRadiusKey]
|
||||
color:[aCoder decodeObjectForKey:CPShadowColorKey]];
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeSize:_offset forKey:CPShadowOffsetKey];
|
||||
[aCoder encodeFloat:_blurRadius forKey:CPShadowBlurRadiusKey];
|
||||
[aCoder encodeObject:_color forKey:CPShadowColorKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -221,7 +221,10 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
|
||||
if (_dividerStyle === CPSplitViewDividerStyleThin)
|
||||
return [self currentValueForThemeAttribute:@"pane-divider-color"];
|
||||
|
||||
return [CPColor colorWithPatternImage:[self currentValueForThemeAttribute:(_isVertical ? @"vertical-divider-color" : @"horizontal-divider-color")]];
|
||||
// A theme may define no divider image, and a pattern needs one.
|
||||
var image = [self currentValueForThemeAttribute:(_isVertical ? @"vertical-divider-color" : @"horizontal-divider-color")];
|
||||
|
||||
return image ? [CPColor colorWithPatternImage:image] : nil;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
||||
@@ -322,6 +322,8 @@ var CPStepperMinValue = @"CPStepperMinValue",
|
||||
_valueWraps = [aCoder decodeBoolForKey:CPStepperValueWraps];
|
||||
_autorepeat = [aCoder decodeBoolForKey:CPStepperAutorepeat];
|
||||
|
||||
// The archive holds the buttons as subviews, and _init creates them again.
|
||||
[self setSubviews:@[]];
|
||||
[self _init];
|
||||
[self setEnabled:[self isEnabled]];
|
||||
}
|
||||
|
||||
+11
-4
@@ -826,9 +826,14 @@ var CPTabViewItemsKey = "CPTabViewItemsKey",
|
||||
var items = [aCoder decodeObjectForKey:CPTabViewItemsKey] || [CPArray array];
|
||||
[self _insertTabViewItems:items atIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [items count])] canUpdateSelectedTab:NO];
|
||||
|
||||
[self setDelegate:[aCoder decodeObjectForKey:CPTabViewDelegateKey]];
|
||||
// Inserting the items selected the first one. This is done before there is a
|
||||
// delegate, because decoding is not a selection made by the user.
|
||||
var selectedItem = [aCoder decodeObjectForKey:CPTabViewSelectedItemKey];
|
||||
|
||||
_selectedTabViewItem = [aCoder decodeObjectForKey:CPTabViewSelectedItemKey];
|
||||
if (selectedItem)
|
||||
[self _selectTabViewItemAtIndex:[self indexOfTabViewItem:selectedItem]];
|
||||
|
||||
[self setDelegate:[aCoder decodeObjectForKey:CPTabViewDelegateKey]];
|
||||
|
||||
_type = [aCoder decodeIntForKey:CPTabViewTypeKey];
|
||||
}
|
||||
@@ -866,15 +871,17 @@ var CPTabViewItemsKey = "CPTabViewItemsKey",
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
// Don't bother to encode the CPBox. We will recreate it on decode and its content view is already
|
||||
// Don't bother to encode the CPBox or the tabs. We will recreate them on decode and its content view is already
|
||||
// stored by the tab view item. Not encoding _box makes the resulting archive smaller and reduces
|
||||
// the surface for decoding bugs (of which we've had many in tab view).
|
||||
var subviews = [self subviews];
|
||||
[_box removeFromSuperview];
|
||||
[_tabs removeFromSuperview];
|
||||
[super encodeWithCoder:aCoder];
|
||||
[self setSubviews:subviews];
|
||||
|
||||
[aCoder encodeObject:_items forKey:CPTabViewItemsKey];
|
||||
// nib2cib stores its items in _items. Otherwise the items are the segments of _tabs.
|
||||
[aCoder encodeObject:_items || [self items] forKey:CPTabViewItemsKey];
|
||||
|
||||
[aCoder encodeConditionalObject:_selectedTabViewItem forKey:CPTabViewSelectedItemKey];
|
||||
|
||||
|
||||
@@ -6269,14 +6269,15 @@ var CPTableViewDataSourceKey = @"CPTableViewDataSourceKey",
|
||||
_headerView = [aCoder decodeObjectForKey:CPTableViewHeaderViewKey];
|
||||
_cornerView = [aCoder decodeObjectForKey:CPTableViewCornerViewKey];
|
||||
|
||||
[self setDataSource:[aCoder decodeObjectForKey:CPTableViewDataSourceKey]];
|
||||
[self setDelegate:[aCoder decodeObjectForKey:CPTableViewDelegateKey]];
|
||||
|
||||
[self _init];
|
||||
|
||||
if ([aCoder containsValueForKey:CPTableViewArchivedReusableViewsKey])
|
||||
_archivedDataViews = [aCoder decodeObjectForKey:CPTableViewArchivedReusableViewsKey];
|
||||
|
||||
// Setting the data source reloads the table, which needs the state set by _init.
|
||||
[self setDataSource:[aCoder decodeObjectForKey:CPTableViewDataSourceKey]];
|
||||
[self setDelegate:[aCoder decodeObjectForKey:CPTableViewDelegateKey]];
|
||||
|
||||
[self _updateIsViewBased];
|
||||
|
||||
[self viewWillMoveToSuperview:[self superview]];
|
||||
|
||||
@@ -2273,7 +2273,8 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
|
||||
CPTextFieldPlaceholderStringKey = "CPTextFieldPlaceholderStringKey",
|
||||
CPTextFieldUsesSingleLineMode = "CPTextFieldUsesSingleLineMode",
|
||||
CPTextFieldWraps = "CPTextFieldWraps",
|
||||
CPTextFieldScrolls = "CPTextFieldScrolls";
|
||||
CPTextFieldScrolls = "CPTextFieldScrolls",
|
||||
CPTextFieldDelegateKey = "CPTextFieldDelegateKey";
|
||||
|
||||
|
||||
@implementation CPTextField (CPCoding)
|
||||
@@ -2303,6 +2304,8 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
|
||||
[self _setUsesSingleLineMode:[aCoder decodeBoolForKey:CPTextFieldUsesSingleLineMode]];
|
||||
[self _setWraps:[aCoder decodeBoolForKey:CPTextFieldWraps]];
|
||||
[self _setScrolls:[aCoder decodeBoolForKey:CPTextFieldScrolls]];
|
||||
|
||||
[self setDelegate:[aCoder decodeObjectForKey:CPTextFieldDelegateKey]];
|
||||
[self updateTrackingAreas];
|
||||
}
|
||||
|
||||
@@ -2332,6 +2335,8 @@ var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
|
||||
[aCoder encodeBool:_usesSingleLineMode forKey:CPTextFieldUsesSingleLineMode];
|
||||
[aCoder encodeBool:_wraps forKey:CPTextFieldWraps];
|
||||
[aCoder encodeBool:_scrolls forKey:CPTextFieldScrolls];
|
||||
|
||||
[aCoder encodeConditionalObject:_delegate forKey:CPTextFieldDelegateKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1741,8 +1741,17 @@ var CPTokenFieldTokenizingCharacterSetKey = "CPTokenFieldTokenizingCharacterSe
|
||||
_completionDelay = [aCoder decodeDoubleForKey:CPTokenFieldCompletionDelayKey] || [[self class] defaultCompletionDelay];
|
||||
_buttonType = [aCoder decodeIntForKey:CPTokenFieldButtonTypeKey] || CPTokenFieldDisclosureButtonType;
|
||||
|
||||
// The archive holds the token scroll view as a subview, and _init creates it again.
|
||||
[self setSubviews:@[]];
|
||||
[self _init];
|
||||
|
||||
// The tokens were created by setObjectValue: before the scroll view existed.
|
||||
var contentView = [_tokenScrollView documentView],
|
||||
tokens = [self _tokens];
|
||||
|
||||
for (var i = 0, count = [tokens count]; i < count; i++)
|
||||
[contentView addSubview:tokens[i]];
|
||||
|
||||
[self setNeedsLayout];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
@@ -121,7 +121,6 @@ CPVisualEffectStateInactive = 2;
|
||||
- (void)_setEffectEnabled:(BOOL)shouldEnable
|
||||
{
|
||||
var dark = [[self appearance] isEqual:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]],
|
||||
prop = CPBrowserStyleProperty("backdrop-filter"),
|
||||
color = (dark ? [CPColor colorWithHexString:@"1e1e1e"] : [CPColor whiteColor]),
|
||||
finalColor = shouldEnable ? [color colorWithAlphaComponent:0.6] : color;
|
||||
|
||||
@@ -129,7 +128,7 @@ CPVisualEffectStateInactive = 2;
|
||||
|
||||
if (CPDOMAvailable)
|
||||
{
|
||||
self._DOMElement.style[prop] = shouldEnable ? "blur(30px)" : nil;
|
||||
self._DOMElement.style[CPBrowserStyleProperty("backdrop-filter")] = shouldEnable ? "blur(30px)" : nil;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -63,3 +63,4 @@ task ("Theme", [$BUILD_CJS_CAPPUCCINO_APPKIT], function()
|
||||
task ("build", ["AppKit", $BUILD_CJS_CAPPUCCINO_APPKIT, "Theme"]);
|
||||
|
||||
CLOBBER.include($BUILD_CJS_CAPPUCCINO_APPKIT);
|
||||
CLOBBER.include(path.join(__dirname, "Build"));
|
||||
@@ -137,3 +137,34 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var CPMapTableKeysKey = @"CPMapTableKeysKey",
|
||||
CPMapTableObjectsKey = @"CPMapTableObjectsKey";
|
||||
|
||||
@implementation CPMapTable (CPCoding)
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
self = [self init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
var keys = [aCoder decodeObjectForKey:CPMapTableKeysKey],
|
||||
objects = [aCoder decodeObjectForKey:CPMapTableObjectsKey];
|
||||
|
||||
keys.forEach(function(aKey, anIndex)
|
||||
{
|
||||
[self setObject:objects[anIndex] forKey:aKey];
|
||||
});
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeObject:Array.from(_map.keys()) forKey:CPMapTableKeysKey];
|
||||
[aCoder encodeObject:Array.from(_map.values()) forKey:CPMapTableObjectsKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -526,7 +526,21 @@
|
||||
type = CPBetweenPredicateOperatorType;
|
||||
}
|
||||
else
|
||||
CPRaiseParseError(self, @"comparison predicate");
|
||||
{
|
||||
/*
|
||||
A selector such as isKindOfClass: is a custom comparison operator. As in
|
||||
Cocoa, a custom selector comparison has no modifier.
|
||||
*/
|
||||
var selector = [self parseCustomSelector];
|
||||
|
||||
if (!selector || modifier !== CPDirectPredicateModifier)
|
||||
CPRaiseParseError(self, @"comparison predicate");
|
||||
|
||||
right = [self parseExpression];
|
||||
p = [CPComparisonPredicate predicateWithLeftExpression:left rightExpression:right customSelector:selector];
|
||||
|
||||
return negate ? [CPCompoundPredicate notPredicateWithSubpredicate:p] : p;
|
||||
}
|
||||
|
||||
if ([self scanString:@"[cd]" intoString:NULL])
|
||||
{
|
||||
@@ -552,6 +566,23 @@
|
||||
return negate ? [CPCompoundPredicate notPredicateWithSubpredicate:p]:p;
|
||||
}
|
||||
|
||||
/*
|
||||
Scans a one-argument selector, such as isKindOfClass:, and returns it. Returns nil
|
||||
and leaves the scan location unchanged when there is none.
|
||||
*/
|
||||
- (SEL)parseCustomSelector
|
||||
{
|
||||
var location = [self scanLocation],
|
||||
name = [self scanCharactersFromSet:[CPCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"]];
|
||||
|
||||
if (name && [self scanString:@":" intoString:NULL])
|
||||
return CPSelectorFromString(name + ":");
|
||||
|
||||
[self setScanLocation:location];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CPExpression)parseExpression
|
||||
{
|
||||
return [self parseBinaryExpression];
|
||||
|
||||
+10
-2
@@ -320,7 +320,7 @@ function _systemTimeZoneFromRuntime()
|
||||
{
|
||||
CPData _data @accessors(property=data, readonly);
|
||||
CPInteger _secondsFromGMT @accessors(property=secondFromGMT, readonly);
|
||||
CPString _abbreviation @accessors(property=abbreviation, readonly);
|
||||
CPString _abbreviation;
|
||||
CPString _name @accessors(property=name, readonly);
|
||||
|
||||
// Set only for zones created with +timeZoneForSecondsFromGMT:, which per
|
||||
@@ -738,6 +738,14 @@ function _systemTimeZoneFromRuntime()
|
||||
Returns nil if the date is nil
|
||||
@return the abbreviation
|
||||
*/
|
||||
/*!
|
||||
Returns the abbreviation for the current date, as abbreviationForDate: does.
|
||||
*/
|
||||
- (CPString)abbreviation
|
||||
{
|
||||
return [self abbreviationForDate:[CPDate date]];
|
||||
}
|
||||
|
||||
- (CPString)abbreviationForDate:(CPDate)date
|
||||
{
|
||||
if (!date)
|
||||
@@ -856,7 +864,7 @@ function _systemTimeZoneFromRuntime()
|
||||
*/
|
||||
- (CPString)description
|
||||
{
|
||||
return [CPString stringWithFormat:@"%s (%s) offset %i", _name, _abbreviation, [self secondsFromGMT]];
|
||||
return [CPString stringWithFormat:@"%s (%s) offset %i", _name, [self abbreviation], [self secondsFromGMT]];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -70,3 +70,4 @@ filedir ($BUILD_CJS_FOUNDATION, ["Foundation"], function()
|
||||
task ("build", ["Foundation", $BUILD_CJS_FOUNDATION]);
|
||||
|
||||
CLOBBER.include($BUILD_CJS_FOUNDATION);
|
||||
CLOBBER.include(path.join(__dirname, "Build"));
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* CPBoxTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
A box that is archived and read back must keep its content view, its title and
|
||||
its internal views, and the content view must remain inside the box.
|
||||
*/
|
||||
@implementation CPBoxTest : OJTestCase
|
||||
{
|
||||
CPBox box;
|
||||
CPView contentView;
|
||||
Function logWarning;
|
||||
CPArray warnings;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
warnings = [];
|
||||
logWarning = CPLog.warn;
|
||||
|
||||
CPLog.warn = function(aMessage)
|
||||
{
|
||||
warnings.push(String(aMessage));
|
||||
};
|
||||
|
||||
box = [[CPBox alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 100.0)];
|
||||
contentView = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 60.0, 40.0)];
|
||||
|
||||
[contentView addSubview:[[CPView alloc] initWithFrame:CGRectMake(3.0, 5.0, 20.0, 10.0)]];
|
||||
[box setContentView:contentView];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
CPLog.warn = logWarning;
|
||||
}
|
||||
|
||||
- (CPBox)archivedBox
|
||||
{
|
||||
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:box]];
|
||||
}
|
||||
|
||||
- (void)testArchivingKeepsTheContentView
|
||||
{
|
||||
var decodedContentView = [[self archivedBox] contentView];
|
||||
|
||||
[self assertNotNull:decodedContentView];
|
||||
[self assert:1 equals:[[decodedContentView subviews] count]];
|
||||
[self assertTrue:CGRectEqualToRect([contentView frame], [decodedContentView frame])];
|
||||
}
|
||||
|
||||
- (void)testArchivingKeepsTheContentViewInsideTheBox
|
||||
{
|
||||
var decodedBox = [self archivedBox],
|
||||
boxView = [[decodedBox contentView] superview];
|
||||
|
||||
[self assertNotNull:boxView];
|
||||
[self assert:decodedBox equals:[boxView superview]];
|
||||
[self assertTrue:[[decodedBox subviews] containsObject:boxView]];
|
||||
}
|
||||
|
||||
- (void)testArchivingLeavesTheBoxUnchanged
|
||||
{
|
||||
var subviews = [box subviews];
|
||||
|
||||
[self archivedBox];
|
||||
|
||||
[self assert:subviews equals:[box subviews]];
|
||||
[self assert:contentView equals:[box contentView]];
|
||||
}
|
||||
|
||||
- (void)testArchivingKeepsTheTitle
|
||||
{
|
||||
[box setTitle:@"Title"];
|
||||
[box setTitlePosition:CPAtTop];
|
||||
|
||||
var decodedBox = [self archivedBox];
|
||||
|
||||
[self assert:@"Title" equals:[decodedBox title]];
|
||||
[self assert:CPAtTop equals:[decodedBox titlePosition]];
|
||||
}
|
||||
|
||||
- (void)testUntitledBoxDecodesWithoutAWarning
|
||||
{
|
||||
[self archivedBox];
|
||||
|
||||
[self assert:[] equals:warnings];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* CPButtonBarTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
CPButtonBar has no Cocoa counterpart. It is the strip of add, remove and action
|
||||
buttons that applications place below a table or outline view, and those buttons
|
||||
are often the only way a person can change the list above them.
|
||||
|
||||
These tests run with no theme loaded. A button bar has to work whatever a theme
|
||||
chooses to define, so the class itself must supply usable buttons.
|
||||
*/
|
||||
@implementation CPButtonBarTest : OJTestCase
|
||||
{
|
||||
CPWindow theWindow;
|
||||
CPButtonBar buttonBar;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
|
||||
|
||||
buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 26.0)];
|
||||
[[theWindow contentView] addSubview:buttonBar];
|
||||
}
|
||||
|
||||
- (void)assertButtonHasStandardSize:(CPView)aButton
|
||||
{
|
||||
[self assert:35.0 equals:CGRectGetWidth([aButton frame])];
|
||||
[self assert:25.0 equals:CGRectGetHeight([aButton frame])];
|
||||
}
|
||||
|
||||
- (void)testPlusButtonHasTheStandardSize
|
||||
{
|
||||
[self assertButtonHasStandardSize:[CPButtonBar plusButton]];
|
||||
}
|
||||
|
||||
- (void)testMinusButtonHasTheStandardSize
|
||||
{
|
||||
[self assertButtonHasStandardSize:[CPButtonBar minusButton]];
|
||||
}
|
||||
|
||||
- (void)testActionPopupButtonHasTheStandardSize
|
||||
{
|
||||
[self assertButtonHasStandardSize:[CPButtonBar actionPopupButton]];
|
||||
}
|
||||
|
||||
/*
|
||||
A button must take up space in the bar and stay inside it, or a person cannot
|
||||
click it. The first button starts one point before the bar, so that its border
|
||||
falls outside the bar's edge.
|
||||
*/
|
||||
- (void)testLayoutPlacesEachButtonInsideTheBar
|
||||
{
|
||||
var buttons = [[CPButtonBar plusButton], [CPButtonBar minusButton], [CPButtonBar actionPopupButton]];
|
||||
|
||||
[buttonBar setButtons:buttons];
|
||||
[buttonBar layoutSubviews];
|
||||
|
||||
buttons.forEach(function(aButton)
|
||||
{
|
||||
[self assert:buttonBar equals:[aButton superview]];
|
||||
[self assertTrue:CGRectGetWidth([aButton frame]) > 0.0];
|
||||
[self assertTrue:CGRectGetMinX([aButton frame]) >= -1.0];
|
||||
[self assertTrue:CGRectGetMaxX([aButton frame]) <= CGRectGetWidth([buttonBar bounds])];
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Adjacent buttons share a one-point border, and they overlap by no more than that.
|
||||
*/
|
||||
- (void)testLayoutPlacesButtonsInOrderSharingABorder
|
||||
{
|
||||
var plusButton = [CPButtonBar plusButton],
|
||||
minusButton = [CPButtonBar minusButton];
|
||||
|
||||
[buttonBar setButtons:[plusButton, minusButton]];
|
||||
[buttonBar layoutSubviews];
|
||||
|
||||
[self assert:CGRectGetMaxX([plusButton frame]) - 1.0 equals:CGRectGetMinX([minusButton frame])];
|
||||
}
|
||||
|
||||
/*
|
||||
A hidden button leaves no gap, and the buttons after it move up.
|
||||
*/
|
||||
- (void)testHidingAButtonClosesTheGap
|
||||
{
|
||||
var plusButton = [CPButtonBar plusButton],
|
||||
minusButton = [CPButtonBar minusButton];
|
||||
|
||||
[buttonBar setButtons:[plusButton, minusButton]];
|
||||
[buttonBar layoutSubviews];
|
||||
|
||||
var firstOrigin = CGRectGetMinX([plusButton frame]);
|
||||
|
||||
[plusButton setHidden:YES];
|
||||
[buttonBar layoutSubviews];
|
||||
|
||||
[self assert:firstOrigin equals:CGRectGetMinX([minusButton frame])];
|
||||
}
|
||||
|
||||
- (void)testRightButtonsAreAlignedToTheRightEdge
|
||||
{
|
||||
var button = [CPButtonBar plusButton];
|
||||
|
||||
[buttonBar setRightButtons:[button]];
|
||||
[buttonBar layoutSubviews];
|
||||
|
||||
[self assert:buttonBar equals:[button superview]];
|
||||
[self assertTrue:CGRectGetWidth([button frame]) > 0.0];
|
||||
[self assertTrue:CGRectGetMaxX([button frame]) >= CGRectGetWidth([buttonBar bounds]) - 1.0];
|
||||
}
|
||||
|
||||
/*
|
||||
Applications add their own buttons to a bar, so any button is acceptable, not
|
||||
only those the class creates.
|
||||
*/
|
||||
- (void)testAnApplicationButtonCanBeAdded
|
||||
{
|
||||
var button = [[CPButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 35.0, 25.0)];
|
||||
|
||||
[buttonBar setButtons:[button]];
|
||||
[buttonBar layoutSubviews];
|
||||
|
||||
[self assert:[button] equals:[buttonBar buttons]];
|
||||
[self assert:buttonBar equals:[button superview]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -104,6 +104,12 @@
|
||||
[self assertTrue:wasClicked];
|
||||
}
|
||||
|
||||
- (void)testNewButtonValueIsOff
|
||||
{
|
||||
[self assert:CPOffState equals:[button objectValue] message:@"a new button should be off"];
|
||||
[self assert:CPOffState equals:[button state] message:@"state should be CPOffState"];
|
||||
}
|
||||
|
||||
- (void)testSetObjectValue
|
||||
{
|
||||
[button setObjectValue:CPOnState];
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* CPObjectArchivingTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
Views hold and share objects that are not views, and those objects are archived
|
||||
with them. An object that is archived and read back must keep the properties that
|
||||
an application reads. DropInArchiveTest.j and CPViewDecodedBehaviorTest.j cover
|
||||
the views.
|
||||
*/
|
||||
|
||||
@implementation CPObjectArchivingTest : OJTestCase
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
}
|
||||
|
||||
- (id)archived:(id)anObject
|
||||
{
|
||||
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:anObject]];
|
||||
}
|
||||
|
||||
// MARK: Foundation collections and values
|
||||
|
||||
- (void)testAnArrayKeepsItsObjects
|
||||
{
|
||||
[self assert:[@"One", 2, YES] equals:[self archived:[@"One", 2, YES]]];
|
||||
}
|
||||
|
||||
- (void)testADictionaryKeepsItsEntries
|
||||
{
|
||||
var dictionary = @{ @"one": @"first", @"two": 2 };
|
||||
|
||||
[self assert:dictionary equals:[self archived:dictionary]];
|
||||
}
|
||||
|
||||
- (void)testASetKeepsItsObjects
|
||||
{
|
||||
var set = [CPSet setWithArray:[@"One", @"Two"]];
|
||||
|
||||
[self assert:set equals:[self archived:set]];
|
||||
}
|
||||
|
||||
- (void)testAnIndexSetKeepsItsIndexes
|
||||
{
|
||||
var indexes = [CPMutableIndexSet indexSetWithIndexesInRange:CPMakeRange(2, 3)];
|
||||
|
||||
[indexes addIndex:9];
|
||||
|
||||
[self assert:indexes equals:[self archived:indexes]];
|
||||
}
|
||||
|
||||
- (void)testAnIndexPathKeepsItsIndexes
|
||||
{
|
||||
var indexPath = [CPIndexPath indexPathWithIndexes:[1, 2, 3] length:3];
|
||||
|
||||
[self assert:indexPath equals:[self archived:indexPath]];
|
||||
}
|
||||
|
||||
- (void)testADateKeepsItsTime
|
||||
{
|
||||
var date = [CPDate dateWithTimeIntervalSince1970:1000.0];
|
||||
|
||||
[self assert:1000.0 equals:[[self archived:date] timeIntervalSince1970]];
|
||||
}
|
||||
|
||||
- (void)testNullStaysTheSingleton
|
||||
{
|
||||
[self assert:[CPNull null] equals:[self archived:[CPNull null]]];
|
||||
}
|
||||
|
||||
- (void)testADecimalNumberKeepsItsValue
|
||||
{
|
||||
var number = [CPDecimalNumber decimalNumberWithString:@"12.5"];
|
||||
|
||||
[self assert:[number stringValue] equals:[[self archived:number] stringValue]];
|
||||
}
|
||||
|
||||
- (void)testAnAttributedStringKeepsItsText
|
||||
{
|
||||
var string = [[CPAttributedString alloc] initWithString:@"Text" attributes:@{ @"key": @"value" }];
|
||||
|
||||
[self assert:string equals:[self archived:string]];
|
||||
}
|
||||
|
||||
- (void)testASortDescriptorKeepsItsDefinition
|
||||
{
|
||||
var descriptor = [CPSortDescriptor sortDescriptorWithKey:@"name" ascending:NO selector:@selector(compare:)],
|
||||
decoded = [self archived:descriptor];
|
||||
|
||||
[self assert:@"name" equals:[decoded key]];
|
||||
[self assertFalse:[decoded ascending]];
|
||||
[self assert:@selector(compare:) equals:[decoded selector]];
|
||||
}
|
||||
|
||||
- (void)testACharacterSetKeepsItsMembers
|
||||
{
|
||||
var decoded = [self archived:[CPCharacterSet characterSetWithCharactersInString:@"ab"]];
|
||||
|
||||
[self assertTrue:[decoded characterIsMember:@"a"]];
|
||||
[self assertFalse:[decoded characterIsMember:@"c"]];
|
||||
}
|
||||
|
||||
- (void)testAnErrorKeepsItsDefinition
|
||||
{
|
||||
var error = [CPError errorWithDomain:@"Domain" code:7 userInfo:@{ @"key": @"value" }],
|
||||
decoded = [self archived:error];
|
||||
|
||||
[self assert:@"Domain" equals:[decoded domain]];
|
||||
[self assert:7 equals:[decoded code]];
|
||||
[self assert:@"value" equals:[[decoded userInfo] objectForKey:@"key"]];
|
||||
}
|
||||
|
||||
- (void)testAnExceptionKeepsItsDefinition
|
||||
{
|
||||
var exception = [CPException exceptionWithName:@"Name" reason:@"Reason" userInfo:nil],
|
||||
decoded = [self archived:exception];
|
||||
|
||||
[self assert:@"Name" equals:[decoded name]];
|
||||
[self assert:@"Reason" equals:[decoded reason]];
|
||||
}
|
||||
|
||||
- (void)testALocaleKeepsItsIdentifier
|
||||
{
|
||||
var locale = [[CPLocale alloc] initWithLocaleIdentifier:@"fr_FR"];
|
||||
|
||||
[self assert:@"fr_FR" equals:[[self archived:locale] localeIdentifier]];
|
||||
}
|
||||
|
||||
- (void)testANumberFormatterKeepsItsStyle
|
||||
{
|
||||
var formatter = [[CPNumberFormatter alloc] init];
|
||||
|
||||
[formatter setNumberStyle:CPNumberFormatterDecimalStyle];
|
||||
|
||||
[self assert:CPNumberFormatterDecimalStyle equals:[[self archived:formatter] numberStyle]];
|
||||
}
|
||||
|
||||
- (void)testADateFormatterKeepsItsFormat
|
||||
{
|
||||
var formatter = [[CPDateFormatter alloc] init];
|
||||
|
||||
[formatter setDateFormat:@"yyyy-MM-dd"];
|
||||
|
||||
[self assert:@"yyyy-MM-dd" equals:[[self archived:formatter] dateFormat]];
|
||||
}
|
||||
|
||||
- (void)testAComparisonPredicateStillEvaluates
|
||||
{
|
||||
var decoded = [self archived:[CPPredicate predicateWithFormat:@"self BEGINSWITH 'a'"]];
|
||||
|
||||
[self assertTrue:[decoded evaluateWithObject:@"apple"]];
|
||||
[self assertFalse:[decoded evaluateWithObject:@"pear"]];
|
||||
}
|
||||
|
||||
- (void)testACompoundPredicateStillEvaluates
|
||||
{
|
||||
var decoded = [self archived:[CPPredicate predicateWithFormat:@"self BEGINSWITH 'a' AND self ENDSWITH 'e'"]];
|
||||
|
||||
[self assertTrue:[decoded evaluateWithObject:@"apple"]];
|
||||
[self assertFalse:[decoded evaluateWithObject:@"apricot"]];
|
||||
}
|
||||
|
||||
// MARK: AppKit values
|
||||
|
||||
- (void)testAColorKeepsItsComponents
|
||||
{
|
||||
var color = [CPColor colorWithRed:0.25 green:0.5 blue:0.75 alpha:0.5];
|
||||
|
||||
[self assert:[color cssString] equals:[[self archived:color] cssString]];
|
||||
}
|
||||
|
||||
- (void)testANamedFontKeepsItsDefinition
|
||||
{
|
||||
var font = [CPFont boldFontWithName:@"Courier" size:21.0],
|
||||
decoded = [self archived:font];
|
||||
|
||||
[self assert:[font cssString] equals:[decoded cssString]];
|
||||
[self assertTrue:[decoded isBold]];
|
||||
}
|
||||
|
||||
- (void)testASystemFontStaysASystemFont
|
||||
{
|
||||
var decoded = [self archived:[CPFont systemFontOfSize:15.0]];
|
||||
|
||||
[self assertTrue:[decoded isSystem]];
|
||||
[self assert:15.0 equals:[decoded size]];
|
||||
}
|
||||
|
||||
- (void)testAnImageKeepsItsDefinition
|
||||
{
|
||||
var image = [[CPImage alloc] initWithContentsOfFile:@"image.png" size:CGSizeMake(16.0, 32.0)],
|
||||
decoded = [self archived:image];
|
||||
|
||||
[self assert:@"image.png" equals:[decoded filename]];
|
||||
[self assertTrue:CGSizeEqualToSize(CGSizeMake(16.0, 32.0), [decoded size])];
|
||||
}
|
||||
|
||||
- (void)testAShadowKeepsItsDefinition
|
||||
{
|
||||
var shadow = [[CPShadow alloc] init];
|
||||
|
||||
[shadow setShadowOffset:CGSizeMake(2.0, 3.0)];
|
||||
[shadow setShadowBlurRadius:4.0];
|
||||
[shadow setShadowColor:[CPColor redColor]];
|
||||
|
||||
var decoded = [self archived:shadow];
|
||||
|
||||
[self assertTrue:CGSizeEqualToSize(CGSizeMake(2.0, 3.0), [decoded shadowOffset])];
|
||||
[self assert:4.0 equals:[decoded shadowBlurRadius]];
|
||||
[self assert:[[CPColor redColor] cssString] equals:[[decoded shadowColor] cssString]];
|
||||
}
|
||||
|
||||
- (void)testANewShadowKeepsItsDefaults
|
||||
{
|
||||
var decoded = [self archived:[[CPShadow alloc] init]];
|
||||
|
||||
[self assertTrue:CGSizeEqualToSize(CGSizeMakeZero(), [decoded shadowOffset])];
|
||||
[self assert:0.0 equals:[decoded shadowBlurRadius]];
|
||||
[self assert:1.0 / 3.0 equals:[[decoded shadowColor] alphaComponent]];
|
||||
}
|
||||
|
||||
- (void)testAParagraphStyleKeepsItsAlignment
|
||||
{
|
||||
var style = [[CPMutableParagraphStyle alloc] init];
|
||||
|
||||
[style setAlignment:CPCenterTextAlignment];
|
||||
|
||||
[self assert:CPCenterTextAlignment equals:[[self archived:style] alignment]];
|
||||
}
|
||||
|
||||
- (void)testAnAppearanceKeepsItsName
|
||||
{
|
||||
var appearance = [CPAppearance appearanceNamed:CPAppearanceNameVibrantDark];
|
||||
|
||||
[self assert:CPAppearanceNameVibrantDark equals:[[self archived:appearance] name]];
|
||||
}
|
||||
|
||||
// MARK: Objects that belong to views
|
||||
|
||||
- (void)testAMenuKeepsItsItems
|
||||
{
|
||||
var menu = [[CPMenu alloc] initWithTitle:@"Menu"],
|
||||
item = [[CPMenuItem alloc] initWithTitle:@"Item" action:@selector(performAction:) keyEquivalent:@"i"],
|
||||
submenu = [[CPMenu alloc] initWithTitle:@""];
|
||||
|
||||
[item setTag:7];
|
||||
[item setState:CPOnState];
|
||||
[item setKeyEquivalentModifierMask:CPShiftKeyMask];
|
||||
[menu addItem:item];
|
||||
[menu addItem:[CPMenuItem separatorItem]];
|
||||
[menu setSubmenu:submenu forItem:[menu addItemWithTitle:@"Parent" action:nil keyEquivalent:@""]];
|
||||
|
||||
var decoded = [self archived:menu],
|
||||
decodedItem = [decoded itemAtIndex:0];
|
||||
|
||||
[self assert:@"Menu" equals:[decoded title]];
|
||||
[self assert:3 equals:[decoded numberOfItems]];
|
||||
[self assert:@"Item" equals:[decodedItem title]];
|
||||
[self assert:@selector(performAction:) equals:[decodedItem action]];
|
||||
[self assert:@"i" equals:[decodedItem keyEquivalent]];
|
||||
[self assert:CPShiftKeyMask equals:[decodedItem keyEquivalentModifierMask]];
|
||||
[self assert:7 equals:[decodedItem tag]];
|
||||
[self assert:CPOnState equals:[decodedItem state]];
|
||||
[self assert:decoded equals:[decodedItem menu]];
|
||||
[self assertTrue:[[decoded itemAtIndex:1] isSeparatorItem]];
|
||||
[self assert:@"Parent" equals:[[[decoded itemAtIndex:2] submenu] title]];
|
||||
[self assert:decoded equals:[[[decoded itemAtIndex:2] submenu] supermenu]];
|
||||
}
|
||||
|
||||
- (void)testATableColumnKeepsItsDefinition
|
||||
{
|
||||
var column = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
|
||||
|
||||
[column setWidth:120.0];
|
||||
[column setMinWidth:50.0];
|
||||
[column setMaxWidth:300.0];
|
||||
[column setEditable:YES];
|
||||
[column setHidden:YES];
|
||||
[[column headerView] setStringValue:@"Title"];
|
||||
[column setSortDescriptorPrototype:[CPSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]];
|
||||
|
||||
var decoded = [self archived:column];
|
||||
|
||||
[self assert:@"Column" equals:[decoded identifier]];
|
||||
[self assert:120.0 equals:[decoded width]];
|
||||
[self assert:50.0 equals:[decoded minWidth]];
|
||||
[self assert:300.0 equals:[decoded maxWidth]];
|
||||
[self assertTrue:[decoded isEditable]];
|
||||
[self assertTrue:[decoded isHidden]];
|
||||
[self assert:@"Title" equals:[[decoded headerView] stringValue]];
|
||||
[self assert:@"name" equals:[[decoded sortDescriptorPrototype] key]];
|
||||
}
|
||||
|
||||
- (void)testATabViewItemKeepsItsDefinition
|
||||
{
|
||||
var item = [[CPTabViewItem alloc] initWithIdentifier:@"identifier"];
|
||||
|
||||
[item setLabel:@"Label"];
|
||||
[item setView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 10.0, 20.0)]];
|
||||
|
||||
var decoded = [self archived:item];
|
||||
|
||||
[self assert:@"identifier" equals:[decoded identifier]];
|
||||
[self assert:@"Label" equals:[decoded label]];
|
||||
[self assertNotNull:[decoded view]];
|
||||
}
|
||||
|
||||
- (void)testAnObjectControllerKeepsItsContent
|
||||
{
|
||||
var controller = [[CPObjectController alloc] initWithContent:@"Content"];
|
||||
|
||||
[controller setEditable:NO];
|
||||
|
||||
var decoded = [self archived:controller];
|
||||
|
||||
[self assert:@"Content" equals:[decoded content]];
|
||||
[self assertFalse:[decoded isEditable]];
|
||||
}
|
||||
|
||||
- (void)testAnArrayControllerKeepsItsConfiguration
|
||||
{
|
||||
var controller = [[CPArrayController alloc] initWithContent:[@"One", @"Two"]];
|
||||
|
||||
[controller setAvoidsEmptySelection:NO];
|
||||
[controller setPreservesSelection:NO];
|
||||
|
||||
var decoded = [self archived:controller];
|
||||
|
||||
[self assertFalse:[decoded avoidsEmptySelection]];
|
||||
[self assertFalse:[decoded preservesSelection]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* CPOutlineViewInteractionTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
An outline view adds a parent to every row, so it reports a drag by item and a drop
|
||||
by parent item and child index. Selection and editing come from CPTableView, and
|
||||
CPTableViewInteractionTest.j covers them. These tests cover what an outline view
|
||||
changes.
|
||||
*/
|
||||
|
||||
var OutlineInteractionDragType = @"OutlineInteractionDragType";
|
||||
|
||||
@implementation _CPOutlineInteractionView : CPOutlineView
|
||||
{
|
||||
CPArray dragCalls @accessors;
|
||||
}
|
||||
|
||||
- (void)dragView:(CPView)aView at:(CGPoint)aLocation offset:(CGSize)mouseOffset event:(CPEvent)anEvent pasteboard:(CPPasteboard)aPasteboard source:(id)aSourceObject slideBack:(BOOL)slideBack
|
||||
{
|
||||
if (!dragCalls)
|
||||
dragCalls = [];
|
||||
|
||||
dragCalls.push(aPasteboard);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPOutlineInteractionDataSource : CPObject
|
||||
{
|
||||
id tree @accessors;
|
||||
CPArray writtenItems @accessors;
|
||||
CPArray validations @accessors;
|
||||
CPArray acceptedDrops @accessors;
|
||||
id retargetItem @accessors;
|
||||
id retargetIndex @accessors;
|
||||
BOOL shouldRetarget @accessors;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
tree = {
|
||||
@"root": [@"A", @"B", @"C"],
|
||||
@"A": [@"A1", @"A2"],
|
||||
@"B": [],
|
||||
@"C": [@"C1"]
|
||||
};
|
||||
validations = [];
|
||||
acceptedDrops = [];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CPArray)childrenOfItem:(id)anItem
|
||||
{
|
||||
return tree[anItem || @"root"] || [];
|
||||
}
|
||||
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView child:(int)anIndex ofItem:(id)anItem
|
||||
{
|
||||
return [self childrenOfItem:anItem][anIndex];
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
|
||||
{
|
||||
return [self childrenOfItem:anItem].length > 0;
|
||||
}
|
||||
|
||||
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
|
||||
{
|
||||
return [self childrenOfItem:anItem].length;
|
||||
}
|
||||
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
|
||||
{
|
||||
return anItem;
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(CPOutlineView)anOutlineView writeItems:(CPArray)theItems toPasteboard:(CPPasteboard)aPasteboard
|
||||
{
|
||||
writtenItems = theItems;
|
||||
|
||||
[aPasteboard declareTypes:[OutlineInteractionDragType] owner:self];
|
||||
[aPasteboard setData:@"items" forType:OutlineInteractionDragType];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id)anInfo proposedItem:(id)anItem proposedChildIndex:(int)anIndex
|
||||
{
|
||||
validations.push({ @"item": anItem, @"index": anIndex });
|
||||
|
||||
if (shouldRetarget)
|
||||
[anOutlineView setDropItem:retargetItem dropChildIndex:retargetIndex];
|
||||
|
||||
return CPDragOperationMove;
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(CPOutlineView)anOutlineView acceptDrop:(id)anInfo item:(id)anItem childIndex:(int)anIndex
|
||||
{
|
||||
acceptedDrops.push({ @"item": anItem, @"index": anIndex });
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPOutlineInteractionDraggingInfo : CPObject
|
||||
{
|
||||
CPPasteboard draggingPasteboard @accessors;
|
||||
CGPoint draggingLocation @accessors;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPOutlineViewInteractionTest : OJTestCase
|
||||
{
|
||||
CPWindow theWindow;
|
||||
_CPOutlineInteractionView outlineView;
|
||||
_CPOutlineInteractionDataSource dataSource;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
|
||||
outlineView = [[_CPOutlineInteractionView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
|
||||
|
||||
var column = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
|
||||
|
||||
[column setWidth:200.0];
|
||||
[outlineView addTableColumn:column];
|
||||
[outlineView setOutlineTableColumn:column];
|
||||
[outlineView setAllowsMultipleSelection:YES];
|
||||
[[theWindow contentView] addSubview:outlineView];
|
||||
|
||||
dataSource = [[_CPOutlineInteractionDataSource alloc] init];
|
||||
|
||||
[outlineView setDataSource:dataSource];
|
||||
[outlineView registerForDraggedTypes:[OutlineInteractionDragType]];
|
||||
[outlineView expandItem:@"A"];
|
||||
[outlineView reloadData];
|
||||
}
|
||||
|
||||
- (CGPoint)pointInItem:(id)anItem
|
||||
{
|
||||
return CGPointMake(100.0, CGRectGetMidY([outlineView rectOfRow:[outlineView rowForItem:anItem]]));
|
||||
}
|
||||
|
||||
- (id)draggingInfoAtPoint:(CGPoint)aPoint
|
||||
{
|
||||
var info = [[_CPOutlineInteractionDraggingInfo alloc] init],
|
||||
pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
|
||||
|
||||
[pasteboard declareTypes:[OutlineInteractionDragType] owner:self];
|
||||
[info setDraggingPasteboard:pasteboard];
|
||||
[info setDraggingLocation:[outlineView convertPoint:aPoint toView:nil]];
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
- (void)sendMouse:(CPEventType)aType atPoint:(CGPoint)aPoint
|
||||
{
|
||||
var event = [CPEvent mouseEventWithType:aType
|
||||
location:[outlineView convertPoint:aPoint toView:nil]
|
||||
modifierFlags:0
|
||||
timestamp:0
|
||||
windowNumber:[theWindow windowNumber]
|
||||
context:nil
|
||||
eventNumber:0
|
||||
clickCount:1
|
||||
pressure:0];
|
||||
|
||||
[CPApp sendEvent:event];
|
||||
[outlineView trackMouse:event];
|
||||
}
|
||||
|
||||
- (void)testDraggingARowWritesItsItem
|
||||
{
|
||||
var start = [self pointInItem:@"A1"];
|
||||
|
||||
[self sendMouse:CPLeftMouseDown atPoint:start];
|
||||
[self sendMouse:CPLeftMouseDragged atPoint:CGPointMake(start.x + 20.0, start.y)];
|
||||
|
||||
[self assert:[@"A1"] equals:[dataSource writtenItems]];
|
||||
[self assert:1 equals:[[outlineView dragCalls] count]];
|
||||
}
|
||||
|
||||
- (void)testDraggingASelectionWritesEachItem
|
||||
{
|
||||
var firstRow = [outlineView rowForItem:@"A1"];
|
||||
|
||||
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(firstRow, 2)] byExtendingSelection:NO];
|
||||
|
||||
var start = [self pointInItem:@"A2"];
|
||||
|
||||
[self sendMouse:CPLeftMouseDown atPoint:start];
|
||||
[self sendMouse:CPLeftMouseDragged atPoint:CGPointMake(start.x + 20.0, start.y)];
|
||||
|
||||
[self assert:[@"A1", @"A2"] equals:[dataSource writtenItems]];
|
||||
}
|
||||
|
||||
- (void)testTheMiddleOfARowProposesADropOnThatItem
|
||||
{
|
||||
[outlineView draggingUpdated:[self draggingInfoAtPoint:[self pointInItem:@"B"]]];
|
||||
|
||||
[self assert:@"B" equals:[dataSource validations][0].item];
|
||||
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource validations][0].index];
|
||||
}
|
||||
|
||||
- (void)testTheTopOfAChildRowProposesADropInItsParent
|
||||
{
|
||||
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"A2"]];
|
||||
|
||||
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, CGRectGetMinY(rect) + 1.0)]];
|
||||
|
||||
[self assert:@"A" equals:[dataSource validations][0].item];
|
||||
[self assert:1 equals:[dataSource validations][0].index];
|
||||
}
|
||||
|
||||
/*
|
||||
Between a child and a root row, either parent is possible, and the horizontal
|
||||
position of the drag chooses between them.
|
||||
*/
|
||||
- (void)testAnUnindentedDropAboveARootRowProposesTheRoot
|
||||
{
|
||||
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"B"]];
|
||||
|
||||
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(5.0, CGRectGetMinY(rect) + 1.0)]];
|
||||
|
||||
[self assertNull:[dataSource validations][0].item];
|
||||
[self assert:1 equals:[dataSource validations][0].index];
|
||||
}
|
||||
|
||||
- (void)testAnIndentedDropAboveARootRowProposesTheEndOfThePreviousParent
|
||||
{
|
||||
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"B"]];
|
||||
|
||||
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, CGRectGetMinY(rect) + 1.0)]];
|
||||
|
||||
[self assert:@"A" equals:[dataSource validations][0].item];
|
||||
[self assert:2 equals:[dataSource validations][0].index];
|
||||
}
|
||||
|
||||
- (void)testBelowTheLastRowProposesADropAtTheEndOfTheRoot
|
||||
{
|
||||
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, 290.0)]];
|
||||
|
||||
[self assertNull:[dataSource validations][0].item];
|
||||
[self assert:3 equals:[dataSource validations][0].index];
|
||||
}
|
||||
|
||||
- (void)testAcceptingADropReceivesTheProposedItem
|
||||
{
|
||||
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
|
||||
|
||||
[outlineView draggingUpdated:info];
|
||||
|
||||
[self assertTrue:[outlineView performDragOperation:info]];
|
||||
[self assert:@"B" equals:[dataSource acceptedDrops][0].item];
|
||||
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource acceptedDrops][0].index];
|
||||
}
|
||||
|
||||
- (void)testDataSourceCanRetargetADropBetweenChildren
|
||||
{
|
||||
[dataSource setShouldRetarget:YES];
|
||||
[dataSource setRetargetItem:@"A"];
|
||||
[dataSource setRetargetIndex:0];
|
||||
|
||||
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
|
||||
|
||||
[outlineView draggingUpdated:info];
|
||||
[outlineView performDragOperation:info];
|
||||
|
||||
[self assert:@"A" equals:[dataSource acceptedDrops][0].item];
|
||||
[self assert:0 equals:[dataSource acceptedDrops][0].index];
|
||||
}
|
||||
|
||||
- (void)testDataSourceCanRetargetADropOnAnItem
|
||||
{
|
||||
[dataSource setShouldRetarget:YES];
|
||||
[dataSource setRetargetItem:@"C"];
|
||||
[dataSource setRetargetIndex:CPOutlineViewDropOnItemIndex];
|
||||
|
||||
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
|
||||
|
||||
[outlineView draggingUpdated:info];
|
||||
[outlineView performDragOperation:info];
|
||||
|
||||
[self assert:@"C" equals:[dataSource acceptedDrops][0].item];
|
||||
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource acceptedDrops][0].index];
|
||||
}
|
||||
|
||||
- (void)testHoveringOverACollapsedItemExpandsIt
|
||||
{
|
||||
[self assertFalse:[outlineView isItemExpanded:@"C"]];
|
||||
|
||||
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
|
||||
[outlineView._dragHoverTimer fire];
|
||||
|
||||
[self assertTrue:[outlineView isItemExpanded:@"C"]];
|
||||
}
|
||||
|
||||
- (void)testAChildIndexCancelsTheHover
|
||||
{
|
||||
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
|
||||
[outlineView setDropItem:@"C" dropChildIndex:0];
|
||||
|
||||
[self assertNull:outlineView._dragHoverTimer];
|
||||
}
|
||||
|
||||
- (void)testEndingADragCancelsTheHover
|
||||
{
|
||||
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
|
||||
[outlineView draggedView:nil endedAt:CGPointMakeZero() operation:CPDragOperationNone];
|
||||
|
||||
[self assertNull:outlineView._dragHoverTimer];
|
||||
[self assertFalse:[outlineView isItemExpanded:@"C"]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
* CPOutlineViewRowContentTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
Applications draw their own content on table and outline rows. A source list
|
||||
draws a count badge beside an item, for example, as SmartFoldersDemo does in its
|
||||
BadgedOutlineView. That content is correct only if the table keeps redrawing it
|
||||
as a person uses the table.
|
||||
|
||||
These tests take such a view through ordinary use: selecting, scrolling, resizing
|
||||
a column, expanding an item, reloading. After each action, the table must ask for
|
||||
its content again, and only for the rows now on screen.
|
||||
|
||||
The tests observe what a subclass observes. They do not depend on which view
|
||||
performs the table's drawing.
|
||||
*/
|
||||
@implementation RowContentItem : CPObject
|
||||
{
|
||||
CPString title @accessors;
|
||||
CPArray children @accessors;
|
||||
}
|
||||
|
||||
+ (id)itemWithTitle:(CPString)aTitle children:(CPArray)someChildren
|
||||
{
|
||||
var item = [[self alloc] init];
|
||||
|
||||
[item setTitle:aTitle];
|
||||
[item setChildren:someChildren];
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
Records what it is asked to draw, in place of drawing. The default implementations
|
||||
need a canvas, which a unit test does not have.
|
||||
*/
|
||||
@implementation RowContentOutlineView : CPOutlineView
|
||||
{
|
||||
CPArray drawnSteps @accessors;
|
||||
CPArray drawnRows @accessors;
|
||||
CPArray drawnClipRects @accessors;
|
||||
}
|
||||
|
||||
- (void)drawBackgroundInClipRect:(CGRect)aRect
|
||||
{
|
||||
[drawnSteps addObject:@"background"];
|
||||
}
|
||||
|
||||
- (void)highlightSelectionInClipRect:(CGRect)aRect
|
||||
{
|
||||
[drawnSteps addObject:@"selection"];
|
||||
}
|
||||
|
||||
- (void)drawGridInClipRect:(CGRect)aRect
|
||||
{
|
||||
[drawnSteps addObject:@"grid"];
|
||||
}
|
||||
|
||||
- (void)drawRow:(CPInteger)aRow clipRect:(CGRect)aRect
|
||||
{
|
||||
[drawnRows addObject:aRow];
|
||||
[drawnClipRects addObject:aRect];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPOutlineViewRowContentTest : OJTestCase
|
||||
{
|
||||
CPWindow theWindow;
|
||||
CPScrollView scrollView;
|
||||
RowContentOutlineView outlineView;
|
||||
CPTableColumn firstColumn;
|
||||
CPTableColumn secondColumn;
|
||||
CPArray rootItems;
|
||||
CPView drawingView;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
|
||||
|
||||
rootItems = [];
|
||||
|
||||
for (var index = 0; index < 40; index++)
|
||||
{
|
||||
var children = [[RowContentItem itemWithTitle:@"Child A" children:[]],
|
||||
[RowContentItem itemWithTitle:@"Child B" children:[]]];
|
||||
|
||||
rootItems.push([RowContentItem itemWithTitle:@"Item" children:children]);
|
||||
}
|
||||
|
||||
firstColumn = [[CPTableColumn alloc] initWithIdentifier:@"First"];
|
||||
secondColumn = [[CPTableColumn alloc] initWithIdentifier:@"Second"];
|
||||
|
||||
outlineView = [[RowContentOutlineView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)];
|
||||
[outlineView setDrawnSteps:[]];
|
||||
[outlineView setDrawnRows:[]];
|
||||
[outlineView setDrawnClipRects:[]];
|
||||
[outlineView addTableColumn:firstColumn];
|
||||
[outlineView addTableColumn:secondColumn];
|
||||
[outlineView setOutlineTableColumn:firstColumn];
|
||||
|
||||
scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)];
|
||||
[scrollView setDocumentView:outlineView];
|
||||
[[theWindow contentView] addSubview:scrollView];
|
||||
|
||||
[outlineView setDataSource:self];
|
||||
[outlineView reloadData];
|
||||
|
||||
[self settle];
|
||||
}
|
||||
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView child:(CPInteger)anIndex ofItem:(id)anItem
|
||||
{
|
||||
return anItem ? [anItem children][anIndex] : rootItems[anIndex];
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
|
||||
{
|
||||
return [[anItem children] count] > 0;
|
||||
}
|
||||
|
||||
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
|
||||
{
|
||||
return anItem ? [[anItem children] count] : rootItems.length;
|
||||
}
|
||||
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
|
||||
{
|
||||
return [anItem title];
|
||||
}
|
||||
|
||||
/*
|
||||
The views that make up the table: the outline view and its subviews. Controls
|
||||
are left out. Data views and disclosure buttons draw themselves, and they need
|
||||
a canvas to do so.
|
||||
*/
|
||||
- (CPArray)tableViews
|
||||
{
|
||||
return [outlineView].concat([outlineView subviews].filter(function(aView)
|
||||
{
|
||||
return ![aView isKindOfClass:[CPControl class]];
|
||||
}));
|
||||
}
|
||||
|
||||
/*
|
||||
Brings the table to rest, as the end of a display cycle does: the layout is
|
||||
current, nothing is waiting to draw, and nothing has been recorded.
|
||||
*/
|
||||
- (void)settle
|
||||
{
|
||||
[outlineView layoutIfNeeded];
|
||||
[[self tableViews] makeObjectsPerformSelector:@selector(displayIfNeeded)];
|
||||
|
||||
[[outlineView drawnSteps] removeAllObjects];
|
||||
[[outlineView drawnRows] removeAllObjects];
|
||||
[[outlineView drawnClipRects] removeAllObjects];
|
||||
}
|
||||
|
||||
/*
|
||||
CPView does not draw without a DOM, so the display cycle cannot reach drawRect:
|
||||
in a unit test. This does its work: it lays the table out, then sends drawRect:
|
||||
to each view that has been marked for display, and only to those.
|
||||
*/
|
||||
- (void)redraw
|
||||
{
|
||||
[outlineView layoutIfNeeded];
|
||||
|
||||
var graphicsPort = { path: NULL, gState: CGGStateCreate(), gStateStack: [] };
|
||||
|
||||
[CPGraphicsContext setCurrentContext:[CPGraphicsContext graphicsContextWithGraphicsPort:graphicsPort flipped:YES]];
|
||||
|
||||
drawingView = nil;
|
||||
|
||||
[self tableViews].forEach(function(aView)
|
||||
{
|
||||
if (![aView needsDisplay])
|
||||
return;
|
||||
|
||||
var rowCount = [[outlineView drawnRows] count];
|
||||
|
||||
[aView drawRect:[aView bounds]];
|
||||
|
||||
if ([[outlineView drawnRows] count] > rowCount)
|
||||
drawingView = aView;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
A badge has to remain visible, including on a selected or alternating row. So the
|
||||
view that draws the row content must stay in front of anything that paints a row.
|
||||
Only controls may sit in front of it. Without a DOM a subview may not yet have
|
||||
its colors, so this does not look at colors. It looks at what is in front.
|
||||
*/
|
||||
- (void)assertNothingCoversRowContent:(CPString)anAction
|
||||
{
|
||||
var subviews = [outlineView subviews],
|
||||
firstCovering = drawingView === outlineView ? 0 : [subviews indexOfObject:drawingView] + 1;
|
||||
|
||||
for (var index = firstCovering; index < [subviews count]; index++)
|
||||
{
|
||||
var aView = subviews[index];
|
||||
|
||||
if ([aView isKindOfClass:[CPControl class]] || [aView isHidden])
|
||||
continue;
|
||||
|
||||
[self fail:anAction + ": " + [aView className] + " covers the row content"];
|
||||
}
|
||||
}
|
||||
|
||||
- (CPArray)rowsOnScreen
|
||||
{
|
||||
var range = [outlineView rowsInRect:[outlineView exposedRect]],
|
||||
rows = [];
|
||||
|
||||
for (var row = range.location; row < CPMaxRange(range); row++)
|
||||
rows.push(row);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/*
|
||||
The table has been redrawn through each of its drawing steps, for exactly the
|
||||
rows on screen, and each row was asked to draw only within the visible part of
|
||||
the table.
|
||||
*/
|
||||
- (void)assertRowContentIsCurrentAfter:(CPString)anAction
|
||||
{
|
||||
[self redraw];
|
||||
|
||||
var rows = [self rowsOnScreen],
|
||||
exposedRect = [outlineView exposedRect];
|
||||
|
||||
[self assert:[@"background", @"selection", @"grid"] equals:[outlineView drawnSteps] message:anAction];
|
||||
[self assert:rows equals:[outlineView drawnRows] message:anAction];
|
||||
|
||||
[outlineView drawnClipRects].forEach(function(aRect)
|
||||
{
|
||||
[self assertTrue:CGRectContainsRect(exposedRect, aRect) message:anAction + ": clip rect is outside the visible area"];
|
||||
});
|
||||
|
||||
[self assertNothingCoversRowContent:anAction];
|
||||
}
|
||||
|
||||
/*
|
||||
Redrawing costs time, and a table can be large. A table with nothing to change
|
||||
has nothing to draw.
|
||||
*/
|
||||
- (void)testNothingIsRedrawnWithoutAChange
|
||||
{
|
||||
[self redraw];
|
||||
|
||||
[self assert:[] equals:[outlineView drawnSteps]];
|
||||
[self assert:[] equals:[outlineView drawnRows]];
|
||||
}
|
||||
|
||||
- (void)testContentIsDrawnWhenTheOutlineFirstAppears
|
||||
{
|
||||
[outlineView setNeedsDisplay:YES];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"first display"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterSelectingARow
|
||||
{
|
||||
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"selecting a row"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterChangingTheSelection
|
||||
{
|
||||
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
|
||||
[self settle];
|
||||
|
||||
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:4] byExtendingSelection:NO];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"changing the selection"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterClearingTheSelection
|
||||
{
|
||||
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
|
||||
[self settle];
|
||||
|
||||
[outlineView deselectAll:nil];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"clearing the selection"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterSelectingAColumn
|
||||
{
|
||||
[outlineView selectColumnIndexes:[CPIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"selecting a column"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterChangingTheGridStyle
|
||||
{
|
||||
[outlineView setGridStyleMask:CPTableViewSolidVerticalGridLineMask];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"changing the grid style"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterChangingTheGridColor
|
||||
{
|
||||
[outlineView setGridColor:[CPColor redColor]];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"changing the grid color"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterChangingTheRowColors
|
||||
{
|
||||
[outlineView setAlternatingRowBackgroundColors:[[CPColor redColor], [CPColor blueColor]]];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"changing the row colors"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterChangingTheHighlightStyle
|
||||
{
|
||||
[outlineView setSelectionHighlightStyle:CPTableViewSelectionHighlightStyleNone];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"changing the highlight style"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterReloadingTheData
|
||||
{
|
||||
[outlineView reloadData];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"reloading the data"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnForTheNewRowsAfterScrolling
|
||||
{
|
||||
var firstRows = [self rowsOnScreen];
|
||||
|
||||
[[scrollView contentView] scrollToPoint:CGPointMake(0.0, 300.0)];
|
||||
|
||||
[self assertFalse:[firstRows isEqual:[self rowsOnScreen]] message:@"scrolling must change the rows on screen"];
|
||||
[self assertRowContentIsCurrentAfter:@"scrolling"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterResizingAColumn
|
||||
{
|
||||
[secondColumn setWidth:40.0];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"resizing a column"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterHidingAColumn
|
||||
{
|
||||
[secondColumn setHidden:YES];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"hiding a column"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterExpandingAnItem
|
||||
{
|
||||
[outlineView expandItem:rootItems[1]];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"expanding an item"];
|
||||
}
|
||||
|
||||
- (void)testContentIsRedrawnAfterCollapsingAnItem
|
||||
{
|
||||
[outlineView expandItem:rootItems[1]];
|
||||
[self settle];
|
||||
|
||||
[outlineView collapseItem:rootItems[1]];
|
||||
|
||||
[self assertRowContentIsCurrentAfter:@"collapsing an item"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -5,12 +5,24 @@
|
||||
|
||||
@implementation CPScrollViewTest : OJTestCase
|
||||
{
|
||||
CPScrollerStyle globalScrollerStyle;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
globalScrollerStyle = [CPScrollView globalScrollerStyle];
|
||||
}
|
||||
|
||||
/*
|
||||
The global scroller style applies to every scroll view, including those in later
|
||||
test suites, so a test that changes it must not leave it changed.
|
||||
*/
|
||||
- (void)tearDown
|
||||
{
|
||||
[CPScrollView setGlobalScrollerStyle:globalScrollerStyle];
|
||||
}
|
||||
|
||||
/*!
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* CPTableViewDrawingTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
NSTableView draws itself as a sequence of named steps: the background, then
|
||||
the selection highlight, then the grid, then the rows. Each step is a method.
|
||||
A subclass overrides one step to change it and inherits the others, so it can
|
||||
extend the table without owning the table's internals.
|
||||
|
||||
CPTableView follows the same design. These tests confirm that each step reaches
|
||||
a subclass. How the table organizes its drawing is its own concern, so the tests
|
||||
observe only what a subclass can observe.
|
||||
*/
|
||||
@implementation DrawingRecordingTableView : CPTableView
|
||||
{
|
||||
CPArray drawnSteps @accessors;
|
||||
CPArray drawnRows @accessors;
|
||||
}
|
||||
|
||||
/*
|
||||
Applications override this method to draw something other than a solid color
|
||||
or alternating rows behind the table's content.
|
||||
*/
|
||||
- (void)drawBackgroundInClipRect:(CGRect)aRect
|
||||
{
|
||||
[drawnSteps addObject:@"background"];
|
||||
}
|
||||
|
||||
/*
|
||||
Applications override this method to change how selected rows appear.
|
||||
In Cocoa this applies to cell-based tables. A view-based NSTableView uses
|
||||
NSTableRowView for this purpose. CPTableView supports the method on its
|
||||
view-based tables, and existing applications depend on it.
|
||||
*/
|
||||
- (void)highlightSelectionInClipRect:(CGRect)aRect
|
||||
{
|
||||
[drawnSteps addObject:@"selection"];
|
||||
}
|
||||
|
||||
/*
|
||||
Cocoa documents this method for all tables: subclasses override it to draw
|
||||
grid lines other than the standard ones.
|
||||
*/
|
||||
- (void)drawGridInClipRect:(CGRect)aRect
|
||||
{
|
||||
[drawnSteps addObject:@"grid"];
|
||||
}
|
||||
|
||||
/*
|
||||
Applications override this method to draw over a row. A source list, for
|
||||
example, can draw a badge beside an item this way. Cocoa also restricts this
|
||||
method to cell-based tables, and CPTableView supports it as it does the
|
||||
selection highlight.
|
||||
*/
|
||||
- (void)drawRow:(CPInteger)aRow clipRect:(CGRect)aRect
|
||||
{
|
||||
[drawnRows addObject:aRow];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPTableViewDrawingTest : OJTestCase
|
||||
{
|
||||
CPWindow theWindow;
|
||||
DrawingRecordingTableView tableView;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
|
||||
|
||||
tableView = [[DrawingRecordingTableView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
|
||||
[tableView setDrawnSteps:[]];
|
||||
[tableView setDrawnRows:[]];
|
||||
|
||||
[tableView addTableColumn:[[CPTableColumn alloc] initWithIdentifier:@"Column"]];
|
||||
[tableView setDataSource:self];
|
||||
|
||||
[[theWindow contentView] addSubview:tableView];
|
||||
[tableView reloadData];
|
||||
}
|
||||
|
||||
- (int)numberOfRowsInTableView:(CPTableView)aTableView
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
|
||||
{
|
||||
return @"Value";
|
||||
}
|
||||
|
||||
/*
|
||||
Unit tests run without a DOM, and CPView does not draw without one, so the
|
||||
display cycle never reaches drawRect: here. This method does what the display
|
||||
cycle does: it sends drawRect: to the table view and to each of its subviews.
|
||||
The graphics port has no backing store, because creating a bitmap context
|
||||
requires a DOM. The tests therefore show that the steps run, not what they draw.
|
||||
*/
|
||||
- (void)display
|
||||
{
|
||||
[tableView load];
|
||||
|
||||
var graphicsPort = { path: NULL, gState: CGGStateCreate(), gStateStack: [] };
|
||||
|
||||
[CPGraphicsContext setCurrentContext:[CPGraphicsContext graphicsContextWithGraphicsPort:graphicsPort flipped:YES]];
|
||||
|
||||
[tableView drawRect:[tableView bounds]];
|
||||
|
||||
var subviews = [tableView subviews],
|
||||
count = [subviews count];
|
||||
|
||||
while (count--)
|
||||
[subviews[count] drawRect:[subviews[count] bounds]];
|
||||
}
|
||||
|
||||
- (void)testDrawGridInClipRectOverrideIsCalled
|
||||
{
|
||||
[self display];
|
||||
|
||||
[self assertTrue:[[tableView drawnSteps] containsObject:@"grid"]];
|
||||
}
|
||||
|
||||
- (void)testDrawBackgroundInClipRectOverrideIsCalled
|
||||
{
|
||||
[self display];
|
||||
|
||||
[self assertTrue:[[tableView drawnSteps] containsObject:@"background"]];
|
||||
}
|
||||
|
||||
- (void)testHighlightSelectionInClipRectOverrideIsCalled
|
||||
{
|
||||
[self display];
|
||||
|
||||
[self assertTrue:[[tableView drawnSteps] containsObject:@"selection"]];
|
||||
}
|
||||
|
||||
- (void)testDrawRowClipRectOverrideIsCalledForEachExposedRow
|
||||
{
|
||||
[self display];
|
||||
|
||||
[self assert:[0, 1, 2] equals:[tableView drawnRows]];
|
||||
}
|
||||
|
||||
/*
|
||||
Each step draws over the one before it. The order is part of the contract,
|
||||
because a subclass that draws a background relies on the selection and the
|
||||
grid appearing above it.
|
||||
*/
|
||||
- (void)testDrawingStepsRunInOrder
|
||||
{
|
||||
[self display];
|
||||
|
||||
[self assert:[@"background", @"selection", @"grid"] equals:[tableView drawnSteps]];
|
||||
}
|
||||
|
||||
/*
|
||||
A color that an application sets belongs to the application. Theme states
|
||||
change often during normal use, for example when the table gains or loses
|
||||
first responder status, and they must leave that color in place.
|
||||
*/
|
||||
- (void)testBackgroundColorSurvivesThemeStateChange
|
||||
{
|
||||
var color = [CPColor redColor];
|
||||
|
||||
[tableView setBackgroundColor:color];
|
||||
[tableView setThemeState:CPThemeStateFirstResponder];
|
||||
[self assert:color equals:[tableView backgroundColor]];
|
||||
|
||||
[tableView unsetThemeState:CPThemeStateFirstResponder];
|
||||
[self assert:color equals:[tableView backgroundColor]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,785 @@
|
||||
/*
|
||||
* CPTableViewInteractionTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
A person selects, edits and drags rows with the mouse and the keyboard. These tests
|
||||
send a table the same events, and check what its delegate and data source receive.
|
||||
|
||||
The drag session itself needs a display, so the table subclass records the call
|
||||
that would start it. A stub stands in for the dragging information. These tests
|
||||
cannot show that a drag image follows the pointer or that a drop indicator is
|
||||
visible.
|
||||
*/
|
||||
|
||||
var InteractionDragType = @"InteractionDragType";
|
||||
|
||||
@implementation _CPTableInteractionTable : CPTableView
|
||||
{
|
||||
CPArray dragCalls @accessors;
|
||||
}
|
||||
|
||||
- (void)dragView:(CPView)aView at:(CGPoint)aLocation offset:(CGSize)mouseOffset event:(CPEvent)anEvent pasteboard:(CPPasteboard)aPasteboard source:(id)aSourceObject slideBack:(BOOL)slideBack
|
||||
{
|
||||
if (!dragCalls)
|
||||
dragCalls = [];
|
||||
|
||||
dragCalls.push({ @"pasteboard": aPasteboard, @"source": aSourceObject });
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPTableInteractionDataSource : CPObject
|
||||
{
|
||||
CPArray rows @accessors;
|
||||
CPArray editedValues @accessors;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
rows = [@"Zero", @"One", @"Two", @"Three", @"Four"];
|
||||
editedValues = [];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (int)numberOfRowsInTableView:(CPTableView)aTableView
|
||||
{
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aColumn row:(int)aRow
|
||||
{
|
||||
return rows[aRow];
|
||||
}
|
||||
|
||||
- (void)tableView:(CPTableView)aTableView setObjectValue:(id)aValue forTableColumn:(CPTableColumn)aColumn row:(int)aRow
|
||||
{
|
||||
editedValues.push({ @"row": aRow, @"value": aValue });
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPTableInteractionDragDataSource : _CPTableInteractionDataSource
|
||||
{
|
||||
BOOL allowsDrag @accessors;
|
||||
CPIndexSet writtenRows @accessors;
|
||||
CPDragOperation validation @accessors;
|
||||
BOOL acceptsDrop @accessors;
|
||||
CPArray validations @accessors;
|
||||
CPArray acceptedDrops @accessors;
|
||||
id retargetRow @accessors;
|
||||
id retargetOperation @accessors;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
allowsDrag = YES;
|
||||
validation = CPDragOperationMove;
|
||||
acceptsDrop = YES;
|
||||
validations = [];
|
||||
acceptedDrops = [];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableView writeRowsWithIndexes:(CPIndexSet)theIndexes toPasteboard:(CPPasteboard)aPasteboard
|
||||
{
|
||||
writtenRows = theIndexes;
|
||||
|
||||
if (allowsDrag)
|
||||
{
|
||||
[aPasteboard declareTypes:[InteractionDragType] owner:self];
|
||||
[aPasteboard setData:@"rows" forType:InteractionDragType];
|
||||
}
|
||||
|
||||
return allowsDrag;
|
||||
}
|
||||
|
||||
- (CPDragOperation)tableView:(CPTableView)aTableView validateDrop:(id)anInfo proposedRow:(int)aRow proposedDropOperation:(CPTableViewDropOperation)anOperation
|
||||
{
|
||||
validations.push({ @"row": aRow, @"operation": anOperation });
|
||||
|
||||
if (retargetRow != nil)
|
||||
[aTableView setDropRow:retargetRow dropOperation:retargetOperation];
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableView acceptDrop:(id)anInfo row:(int)aRow dropOperation:(CPTableViewDropOperation)anOperation
|
||||
{
|
||||
acceptedDrops.push({ @"row": aRow, @"operation": anOperation });
|
||||
|
||||
return acceptsDrop;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPTableInteractionDelegate : CPObject
|
||||
{
|
||||
CPArray calls @accessors;
|
||||
BOOL allowsSelectionChange @accessors;
|
||||
BOOL allowsEditing @accessors;
|
||||
CPArray refusedRows @accessors;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
calls = [];
|
||||
allowsSelectionChange = YES;
|
||||
allowsEditing = YES;
|
||||
refusedRows = [];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)selectionShouldChangeInTableView:(CPTableView)aTableView
|
||||
{
|
||||
calls.push(@"selectionShouldChange");
|
||||
|
||||
return allowsSelectionChange;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableView shouldSelectRow:(int)aRow
|
||||
{
|
||||
return refusedRows.indexOf(aRow) === -1;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(CPTableView)aTableView shouldEditTableColumn:(CPTableColumn)aColumn row:(int)aRow
|
||||
{
|
||||
calls.push(@"shouldEdit");
|
||||
|
||||
return allowsEditing;
|
||||
}
|
||||
|
||||
- (void)tableViewSelectionIsChanging:(CPNotification)aNotification
|
||||
{
|
||||
calls.push(@"selectionIsChanging");
|
||||
}
|
||||
|
||||
- (void)tableViewSelectionDidChange:(CPNotification)aNotification
|
||||
{
|
||||
calls.push(@"selectionDidChange");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPTableInteractionProposalDelegate : CPObject
|
||||
{
|
||||
}
|
||||
|
||||
- (CPIndexSet)tableView:(CPTableView)aTableView selectionIndexesForProposedSelection:(CPIndexSet)theIndexes
|
||||
{
|
||||
return [CPIndexSet indexSetWithIndex:3];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPTableInteractionDraggingInfo : CPObject
|
||||
{
|
||||
CPPasteboard draggingPasteboard @accessors;
|
||||
CGPoint draggingLocation @accessors;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPTableViewInteractionTest : OJTestCase
|
||||
{
|
||||
CPWindow theWindow;
|
||||
_CPTableInteractionTable tableView;
|
||||
CPTableColumn tableColumn;
|
||||
_CPTableInteractionDataSource dataSource;
|
||||
_CPTableInteractionDelegate delegate;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
|
||||
tableView = [[_CPTableInteractionTable alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
|
||||
tableColumn = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
|
||||
|
||||
[tableColumn setWidth:200.0];
|
||||
[tableColumn setEditable:YES];
|
||||
[tableView addTableColumn:tableColumn];
|
||||
[tableView setAllowsMultipleSelection:YES];
|
||||
[[theWindow contentView] addSubview:tableView];
|
||||
|
||||
delegate = [[_CPTableInteractionDelegate alloc] init];
|
||||
[self useDataSource:[[_CPTableInteractionDataSource alloc] init]];
|
||||
[tableView setDelegate:delegate];
|
||||
}
|
||||
|
||||
- (void)useDataSource:(id)aDataSource
|
||||
{
|
||||
dataSource = aDataSource;
|
||||
|
||||
[tableView setDataSource:dataSource];
|
||||
[tableView reloadData];
|
||||
}
|
||||
|
||||
- (CGPoint)pointInRow:(int)aRow
|
||||
{
|
||||
return CGPointMake(20.0, CGRectGetMidY([tableView rectOfRow:aRow]));
|
||||
}
|
||||
|
||||
- (CPEvent)mouseEvent:(CPEventType)aType atPoint:(CGPoint)aPoint flags:(unsigned)theFlags clicks:(int)aClickCount
|
||||
{
|
||||
return [CPEvent mouseEventWithType:aType
|
||||
location:[tableView convertPoint:aPoint toView:nil]
|
||||
modifierFlags:theFlags
|
||||
timestamp:0
|
||||
windowNumber:[theWindow windowNumber]
|
||||
context:nil
|
||||
eventNumber:0
|
||||
clickCount:aClickCount
|
||||
pressure:0];
|
||||
}
|
||||
|
||||
- (void)sendEvent:(CPEvent)anEvent
|
||||
{
|
||||
[CPApp sendEvent:anEvent];
|
||||
[tableView trackMouse:anEvent];
|
||||
}
|
||||
|
||||
- (void)clickPoint:(CGPoint)aPoint flags:(unsigned)theFlags clicks:(int)aClickCount
|
||||
{
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseDown atPoint:aPoint flags:theFlags clicks:aClickCount]];
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseUp atPoint:aPoint flags:theFlags clicks:aClickCount]];
|
||||
}
|
||||
|
||||
- (void)clickRow:(int)aRow
|
||||
{
|
||||
[self clickPoint:[self pointInRow:aRow] flags:0 clicks:1];
|
||||
}
|
||||
|
||||
- (void)clickRow:(int)aRow flags:(unsigned)theFlags
|
||||
{
|
||||
[self clickPoint:[self pointInRow:aRow] flags:theFlags clicks:1];
|
||||
}
|
||||
|
||||
- (void)pressKey:(CPString)aCharacter flags:(unsigned)theFlags
|
||||
{
|
||||
[tableView keyDown:[CPEvent keyEventWithType:CPKeyDown
|
||||
location:CGPointMakeZero()
|
||||
modifierFlags:theFlags
|
||||
timestamp:0
|
||||
windowNumber:[theWindow windowNumber]
|
||||
context:nil
|
||||
characters:aCharacter
|
||||
charactersIgnoringModifiers:aCharacter
|
||||
isARepeat:NO
|
||||
keyCode:0]];
|
||||
}
|
||||
|
||||
- (CPArray)selectedRows
|
||||
{
|
||||
var rows = [];
|
||||
|
||||
[[tableView selectedRowIndexes] getIndexes:rows maxCount:-1 inIndexRange:nil];
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
- (id)draggingInfoAtPoint:(CGPoint)aPoint
|
||||
{
|
||||
var info = [[_CPTableInteractionDraggingInfo alloc] init],
|
||||
pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
|
||||
|
||||
[pasteboard declareTypes:[InteractionDragType] owner:self];
|
||||
[info setDraggingPasteboard:pasteboard];
|
||||
[info setDraggingLocation:[tableView convertPoint:aPoint toView:nil]];
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
- (void)useDragDataSource
|
||||
{
|
||||
[self useDataSource:[[_CPTableInteractionDragDataSource alloc] init]];
|
||||
[tableView registerForDraggedTypes:[InteractionDragType]];
|
||||
}
|
||||
|
||||
// MARK: Selection
|
||||
|
||||
- (void)testClickSelectsARow
|
||||
{
|
||||
[self clickRow:2];
|
||||
|
||||
[self assert:[2] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testClickReplacesTheSelection
|
||||
{
|
||||
[self clickRow:1];
|
||||
[self clickRow:3];
|
||||
|
||||
[self assert:[3] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testClickOutsideTheRowsClearsTheSelection
|
||||
{
|
||||
[self clickRow:1];
|
||||
[self clickPoint:CGPointMake(20.0, 290.0) flags:0 clicks:1];
|
||||
|
||||
[self assert:[] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testClickOutsideTheRowsKeepsTheSelectionWhenItCannotBeEmpty
|
||||
{
|
||||
[tableView setAllowsEmptySelection:NO];
|
||||
[self clickRow:1];
|
||||
[self clickPoint:CGPointMake(20.0, 290.0) flags:0 clicks:1];
|
||||
|
||||
[self assert:[1] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testCommandClickAddsAndRemovesARow
|
||||
{
|
||||
[self clickRow:1];
|
||||
[self clickRow:3 flags:CPCommandKeyMask];
|
||||
|
||||
[self assert:[1, 3] equals:[self selectedRows]];
|
||||
|
||||
[self clickRow:1 flags:CPCommandKeyMask];
|
||||
|
||||
[self assert:[3] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testCommandClickReplacesTheSelectionWithoutMultipleSelection
|
||||
{
|
||||
[tableView setAllowsMultipleSelection:NO];
|
||||
[self clickRow:1];
|
||||
[self clickRow:3 flags:CPCommandKeyMask];
|
||||
|
||||
[self assert:[3] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testShiftClickExtendsFromTheAnchor
|
||||
{
|
||||
[self clickRow:1];
|
||||
[self clickRow:3 flags:CPShiftKeyMask];
|
||||
|
||||
[self assert:[1, 2, 3] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testDraggingAcrossRowsExtendsTheSelection
|
||||
{
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseDown atPoint:[self pointInRow:1] flags:0 clicks:1]];
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseDragged atPoint:[self pointInRow:2] flags:0 clicks:1]];
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseDragged atPoint:[self pointInRow:3] flags:0 clicks:1]];
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseUp atPoint:[self pointInRow:3] flags:0 clicks:1]];
|
||||
|
||||
[self assert:[1, 2, 3] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testDelegateCanRefuseASelectionChange
|
||||
{
|
||||
[delegate setAllowsSelectionChange:NO];
|
||||
[self clickRow:2];
|
||||
|
||||
[self assert:[] equals:[self selectedRows]];
|
||||
[self assertTrue:[delegate calls].indexOf(@"selectionShouldChange") !== -1];
|
||||
}
|
||||
|
||||
- (void)testDelegateCanRefuseARow
|
||||
{
|
||||
[delegate setRefusedRows:[2]];
|
||||
[self clickRow:2];
|
||||
|
||||
[self assert:[] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testDelegateCanReplaceTheProposedSelection
|
||||
{
|
||||
[tableView setDelegate:[[_CPTableInteractionProposalDelegate alloc] init]];
|
||||
[self clickRow:1];
|
||||
|
||||
[self assert:[3] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testAClickReportsOneSelectionChange
|
||||
{
|
||||
[self clickRow:2];
|
||||
|
||||
var changes = [delegate calls].filter(function(aCall)
|
||||
{
|
||||
return aCall === @"selectionDidChange";
|
||||
});
|
||||
|
||||
[self assert:1 equals:changes.length];
|
||||
}
|
||||
|
||||
- (void)testClickingTheSelectedRowReportsNoChange
|
||||
{
|
||||
[self clickRow:2];
|
||||
[delegate setCalls:[]];
|
||||
[self clickRow:2];
|
||||
|
||||
[self assertTrue:[delegate calls].indexOf(@"selectionDidChange") === -1];
|
||||
}
|
||||
|
||||
- (void)testSelectAllSelectsEveryRow
|
||||
{
|
||||
[tableView selectAll:nil];
|
||||
|
||||
[self assert:[0, 1, 2, 3, 4] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testDownArrowMovesTheSelection
|
||||
{
|
||||
[self clickRow:1];
|
||||
[self pressKey:CPDownArrowFunctionKey flags:0];
|
||||
|
||||
[self assert:[2] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testUpArrowMovesTheSelection
|
||||
{
|
||||
[self clickRow:2];
|
||||
[self pressKey:CPUpArrowFunctionKey flags:0];
|
||||
|
||||
[self assert:[1] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testArrowStopsAtTheLastRow
|
||||
{
|
||||
[self clickRow:4];
|
||||
[self pressKey:CPDownArrowFunctionKey flags:0];
|
||||
|
||||
[self assert:[4] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testArrowSelectsAnEndRowWhenNothingIsSelected
|
||||
{
|
||||
[self pressKey:CPDownArrowFunctionKey flags:0];
|
||||
[self assert:[0] equals:[self selectedRows]];
|
||||
|
||||
[tableView deselectAll:nil];
|
||||
[self pressKey:CPUpArrowFunctionKey flags:0];
|
||||
[self assert:[4] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testShiftArrowExtendsAndShrinksTheSelection
|
||||
{
|
||||
[self clickRow:1];
|
||||
[self pressKey:CPDownArrowFunctionKey flags:CPShiftKeyMask];
|
||||
|
||||
[self assert:[1, 2] equals:[self selectedRows]];
|
||||
|
||||
[self pressKey:CPUpArrowFunctionKey flags:CPShiftKeyMask];
|
||||
|
||||
[self assert:[1] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testArrowSkipsARefusedRow
|
||||
{
|
||||
[delegate setRefusedRows:[2]];
|
||||
[self clickRow:1];
|
||||
[self pressKey:CPDownArrowFunctionKey flags:0];
|
||||
|
||||
[self assert:[3] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testArrowRespectsARefusedSelectionChange
|
||||
{
|
||||
[self clickRow:1];
|
||||
[delegate setAllowsSelectionChange:NO];
|
||||
[self pressKey:CPDownArrowFunctionKey flags:0];
|
||||
|
||||
[self assert:[1] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
// MARK: Editing
|
||||
|
||||
- (void)testEditingAnUnselectedRowRaises
|
||||
{
|
||||
[self assertThrows:function()
|
||||
{
|
||||
[tableView editColumn:0 row:1 withEvent:nil select:YES];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testEditingReportsTheEditedCell
|
||||
{
|
||||
[self clickRow:1];
|
||||
[tableView editColumn:0 row:1 withEvent:nil select:YES];
|
||||
|
||||
[self assert:0 equals:[tableView editedColumn]];
|
||||
[self assert:1 equals:[tableView editedRow]];
|
||||
}
|
||||
|
||||
- (void)testEditingMakesTheCellTheFirstResponder
|
||||
{
|
||||
[self clickRow:1];
|
||||
[tableView editColumn:0 row:1 withEvent:nil select:YES];
|
||||
|
||||
[self assertTrue:[[theWindow firstResponder] isKindOfClass:[CPTextField class]]];
|
||||
[self assert:@"One" equals:[[theWindow firstResponder] stringValue]];
|
||||
}
|
||||
|
||||
- (void)testEndingAnEditCommitsTheValue
|
||||
{
|
||||
[self clickRow:1];
|
||||
[tableView editColumn:0 row:1 withEvent:nil select:YES];
|
||||
|
||||
var field = [theWindow firstResponder];
|
||||
|
||||
[field setStringValue:@"Edited"];
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPControlTextDidEndEditingNotification object:field];
|
||||
|
||||
[self assert:1 equals:[dataSource editedValues].length];
|
||||
[self assert:1 equals:[dataSource editedValues][0].row];
|
||||
[self assert:@"Edited" equals:[dataSource editedValues][0].value];
|
||||
[self assert:tableView equals:[theWindow firstResponder]];
|
||||
}
|
||||
|
||||
- (void)testDoubleClickStartsEditing
|
||||
{
|
||||
[self clickRow:1];
|
||||
[self clickPoint:[self pointInRow:1] flags:0 clicks:2];
|
||||
|
||||
[self assert:1 equals:[tableView editedRow]];
|
||||
[self assertTrue:[delegate calls].indexOf(@"shouldEdit") !== -1];
|
||||
}
|
||||
|
||||
- (void)testDelegateCanRefuseAnEdit
|
||||
{
|
||||
[delegate setAllowsEditing:NO];
|
||||
[self clickRow:1];
|
||||
[self clickPoint:[self pointInRow:1] flags:0 clicks:2];
|
||||
|
||||
[self assert:CPNotFound equals:[tableView editedRow]];
|
||||
}
|
||||
|
||||
- (void)testAColumnThatIsNotEditableDoesNotStartAnEdit
|
||||
{
|
||||
[tableColumn setEditable:NO];
|
||||
[self clickRow:1];
|
||||
[self clickPoint:[self pointInRow:1] flags:0 clicks:2];
|
||||
|
||||
[self assert:CPNotFound equals:[tableView editedRow]];
|
||||
}
|
||||
|
||||
// MARK: Starting a drag
|
||||
|
||||
- (void)dragFromRow:(int)aRow toPoint:(CGPoint)aPoint
|
||||
{
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseDown atPoint:[self pointInRow:aRow] flags:0 clicks:1]];
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseDragged atPoint:aPoint flags:0 clicks:1]];
|
||||
}
|
||||
|
||||
- (void)testADraggableTableSelectsOnMouseUp
|
||||
{
|
||||
[self useDragDataSource];
|
||||
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseDown atPoint:[self pointInRow:2] flags:0 clicks:1]];
|
||||
[self assert:[] equals:[self selectedRows]];
|
||||
|
||||
[self sendEvent:[self mouseEvent:CPLeftMouseUp atPoint:[self pointInRow:2] flags:0 clicks:1]];
|
||||
[self assert:[2] equals:[self selectedRows]];
|
||||
}
|
||||
|
||||
- (void)testDraggingARowStartsADrag
|
||||
{
|
||||
[self useDragDataSource];
|
||||
|
||||
var start = [self pointInRow:2];
|
||||
|
||||
[self dragFromRow:2 toPoint:CGPointMake(start.x + 20.0, start.y)];
|
||||
|
||||
[self assert:1 equals:[[tableView dragCalls] count]];
|
||||
[self assert:[CPIndexSet indexSetWithIndex:2] equals:[dataSource writtenRows]];
|
||||
[self assert:[InteractionDragType] equals:[[tableView dragCalls][0].pasteboard types]];
|
||||
[self assert:tableView equals:[tableView dragCalls][0].source];
|
||||
}
|
||||
|
||||
- (void)testDraggingASelectedRowDragsTheWholeSelection
|
||||
{
|
||||
[self useDragDataSource];
|
||||
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(1, 2)] byExtendingSelection:NO];
|
||||
|
||||
var start = [self pointInRow:2];
|
||||
|
||||
[self dragFromRow:2 toPoint:CGPointMake(start.x + 20.0, start.y)];
|
||||
|
||||
[self assert:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(1, 2)] equals:[dataSource writtenRows]];
|
||||
}
|
||||
|
||||
- (void)testAMoveWithinTheClickSlopDoesNotStartADrag
|
||||
{
|
||||
[self useDragDataSource];
|
||||
|
||||
var start = [self pointInRow:2];
|
||||
|
||||
[self dragFromRow:2 toPoint:CGPointMake(start.x + 2.0, start.y)];
|
||||
|
||||
[self assertNull:[tableView dragCalls]];
|
||||
}
|
||||
|
||||
- (void)testDataSourceCanRefuseADrag
|
||||
{
|
||||
[self useDragDataSource];
|
||||
[dataSource setAllowsDrag:NO];
|
||||
|
||||
var start = [self pointInRow:2];
|
||||
|
||||
[self dragFromRow:2 toPoint:CGPointMake(start.x + 20.0, start.y)];
|
||||
|
||||
[self assertNull:[tableView dragCalls]];
|
||||
}
|
||||
|
||||
// MARK: Dropping
|
||||
|
||||
- (void)testAnUnregisteredTypeIsRefused
|
||||
{
|
||||
[self useDragDataSource];
|
||||
[tableView unregisterDraggedTypes];
|
||||
|
||||
[self assert:CPDragOperationNone equals:[tableView draggingEntered:[self draggingInfoAtPoint:[self pointInRow:2]]]];
|
||||
[self assert:0 equals:[dataSource validations].length];
|
||||
}
|
||||
|
||||
- (void)testARegisteredTypeIsValidated
|
||||
{
|
||||
[self useDragDataSource];
|
||||
|
||||
[self assert:CPDragOperationMove equals:[tableView draggingEntered:[self draggingInfoAtPoint:[self pointInRow:2]]]];
|
||||
[self assert:1 equals:[dataSource validations].length];
|
||||
}
|
||||
|
||||
- (void)testValidationCanRefuseADrop
|
||||
{
|
||||
[self useDragDataSource];
|
||||
[dataSource setValidation:CPDragOperationNone];
|
||||
|
||||
[self assert:CPDragOperationNone equals:[tableView draggingUpdated:[self draggingInfoAtPoint:[self pointInRow:2]]]];
|
||||
}
|
||||
|
||||
- (void)testTheMiddleOfARowProposesADropOnThatRow
|
||||
{
|
||||
[self useDragDataSource];
|
||||
[tableView draggingUpdated:[self draggingInfoAtPoint:[self pointInRow:2]]];
|
||||
|
||||
[self assert:2 equals:[dataSource validations][0].row];
|
||||
[self assert:CPTableViewDropOn equals:[dataSource validations][0].operation];
|
||||
}
|
||||
|
||||
- (void)testTheTopOfARowProposesADropAboveThatRow
|
||||
{
|
||||
[self useDragDataSource];
|
||||
|
||||
var rect = [tableView rectOfRow:2];
|
||||
|
||||
[tableView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(20.0, CGRectGetMinY(rect) + 1.0)]];
|
||||
|
||||
[self assert:2 equals:[dataSource validations][0].row];
|
||||
[self assert:CPTableViewDropAbove equals:[dataSource validations][0].operation];
|
||||
}
|
||||
|
||||
- (void)testTheBottomOfARowProposesADropAboveTheNextRow
|
||||
{
|
||||
[self useDragDataSource];
|
||||
|
||||
var rect = [tableView rectOfRow:2];
|
||||
|
||||
[tableView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(20.0, CGRectGetMaxY(rect) - 1.0)]];
|
||||
|
||||
[self assert:3 equals:[dataSource validations][0].row];
|
||||
[self assert:CPTableViewDropAbove equals:[dataSource validations][0].operation];
|
||||
}
|
||||
|
||||
- (void)testBelowTheLastRowProposesADropAtTheEnd
|
||||
{
|
||||
[self useDragDataSource];
|
||||
[tableView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(20.0, 290.0)]];
|
||||
|
||||
[self assert:5 equals:[dataSource validations][0].row];
|
||||
[self assert:CPTableViewDropAbove equals:[dataSource validations][0].operation];
|
||||
}
|
||||
|
||||
- (void)testAcceptingADropReceivesTheProposedRow
|
||||
{
|
||||
[self useDragDataSource];
|
||||
|
||||
var info = [self draggingInfoAtPoint:[self pointInRow:2]];
|
||||
|
||||
[tableView draggingUpdated:info];
|
||||
|
||||
[self assertTrue:[tableView prepareForDragOperation:info]];
|
||||
[self assertTrue:[tableView performDragOperation:info]];
|
||||
[self assert:2 equals:[dataSource acceptedDrops][0].row];
|
||||
[self assert:CPTableViewDropOn equals:[dataSource acceptedDrops][0].operation];
|
||||
}
|
||||
|
||||
- (void)testDataSourceCanRefuseADrop
|
||||
{
|
||||
[self useDragDataSource];
|
||||
[dataSource setAcceptsDrop:NO];
|
||||
|
||||
var info = [self draggingInfoAtPoint:[self pointInRow:2]];
|
||||
|
||||
[tableView draggingUpdated:info];
|
||||
|
||||
[self assertFalse:[tableView performDragOperation:info]];
|
||||
}
|
||||
|
||||
- (void)testDataSourceCanRetargetADrop
|
||||
{
|
||||
[self useDragDataSource];
|
||||
[dataSource setRetargetRow:4];
|
||||
[dataSource setRetargetOperation:CPTableViewDropAbove];
|
||||
|
||||
var info = [self draggingInfoAtPoint:[self pointInRow:2]];
|
||||
|
||||
[tableView draggingUpdated:info];
|
||||
[tableView performDragOperation:info];
|
||||
|
||||
[self assert:4 equals:[dataSource acceptedDrops][0].row];
|
||||
[self assert:CPTableViewDropAbove equals:[dataSource acceptedDrops][0].operation];
|
||||
}
|
||||
|
||||
- (void)testPreparingADropNeedsValidation
|
||||
{
|
||||
var info = [self draggingInfoAtPoint:[self pointInRow:2]];
|
||||
|
||||
[self assertFalse:[tableView prepareForDragOperation:info]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* CPViewDecodedBehaviorTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
DropInArchiveTest.j checks that a decoded view has the right content. These tests
|
||||
check that it still works: that its connections were restored, and that the
|
||||
objects a class creates or observes for itself were set up again.
|
||||
|
||||
A target or delegate is encoded conditionally, so each test archives a container
|
||||
that holds the target too. Bindings are not here. They are stored in cibs as
|
||||
connectors, and not in the views.
|
||||
*/
|
||||
|
||||
@implementation _CPDecodedTarget : CPView
|
||||
{
|
||||
int actionCount @accessors;
|
||||
id lastSender @accessors;
|
||||
}
|
||||
|
||||
- (void)performAction:(id)aSender
|
||||
{
|
||||
actionCount++;
|
||||
lastSender = aSender;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPDecodedTableDataSource : CPObject
|
||||
{
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
return [self init];
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
}
|
||||
|
||||
- (int)numberOfRowsInTableView:(CPTableView)aTableView
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aColumn row:(int)aRow
|
||||
{
|
||||
return @"Row";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation _CPDecodedOutlineDataSource : _CPDecodedTableDataSource
|
||||
{
|
||||
}
|
||||
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView child:(int)anIndex ofItem:(id)anItem
|
||||
{
|
||||
return anItem ? @"Child" : @"Parent";
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
|
||||
{
|
||||
return anItem === @"Parent";
|
||||
}
|
||||
|
||||
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
|
||||
{
|
||||
return (!anItem || anItem === @"Parent") ? 1 : 0;
|
||||
}
|
||||
|
||||
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
|
||||
{
|
||||
return anItem;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPViewDecodedBehaviorTest : OJTestCase
|
||||
{
|
||||
CPView container;
|
||||
_CPDecodedTarget target;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
container = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
|
||||
target = [[_CPDecodedTarget alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[container addSubview:target];
|
||||
}
|
||||
|
||||
- (id)archived:(id)anObject
|
||||
{
|
||||
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:anObject]];
|
||||
}
|
||||
|
||||
/*
|
||||
Connects a control to the target, archives the container, and returns the decoded
|
||||
control. The decoded target is its action's receiver.
|
||||
*/
|
||||
- (id)decodedConnectedControl:(CPControl)aControl
|
||||
{
|
||||
[aControl setTarget:target];
|
||||
[aControl setAction:@selector(performAction:)];
|
||||
[container addSubview:aControl];
|
||||
|
||||
return [[self archived:container] subviews][1];
|
||||
}
|
||||
|
||||
// MARK: Connections
|
||||
|
||||
- (void)testAControlKeepsItsTargetAndAction
|
||||
{
|
||||
var decoded = [self decodedConnectedControl:[[CPButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 80.0, 24.0)]],
|
||||
decodedTarget = [[decoded superview] subviews][0];
|
||||
|
||||
[self assert:decodedTarget equals:[decoded target]];
|
||||
[self assert:@selector(performAction:) equals:[decoded action]];
|
||||
}
|
||||
|
||||
- (void)testADecodedButtonSendsItsAction
|
||||
{
|
||||
var decoded = [self decodedConnectedControl:[[CPButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 80.0, 24.0)]];
|
||||
|
||||
[decoded performClick:nil];
|
||||
|
||||
[self assert:1 equals:[[decoded target] actionCount]];
|
||||
[self assert:decoded equals:[[decoded target] lastSender]];
|
||||
}
|
||||
|
||||
- (void)testADecodedSliderSendsItsAction
|
||||
{
|
||||
var decoded = [self decodedConnectedControl:[[CPSlider alloc] initWithFrame:CGRectMake(0.0, 0.0, 80.0, 24.0)]];
|
||||
|
||||
[decoded sendAction:[decoded action] to:[decoded target]];
|
||||
|
||||
[self assert:1 equals:[[decoded target] actionCount]];
|
||||
}
|
||||
|
||||
- (void)testADecodedTextFieldKeepsItsDelegate
|
||||
{
|
||||
var field = [[CPTextField alloc] initWithFrame:CGRectMake(0.0, 0.0, 80.0, 24.0)];
|
||||
|
||||
[field setDelegate:target];
|
||||
[container addSubview:field];
|
||||
|
||||
var decoded = [[self archived:container] subviews][1];
|
||||
|
||||
[self assert:[[decoded superview] subviews][0] equals:[decoded delegate]];
|
||||
}
|
||||
|
||||
- (void)testADecodedTabViewKeepsItsDelegate
|
||||
{
|
||||
var tabView = [[CPTabView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
|
||||
|
||||
[tabView setDelegate:target];
|
||||
[container addSubview:tabView];
|
||||
|
||||
var decoded = [[self archived:container] subviews][1];
|
||||
|
||||
[self assert:[[decoded superview] subviews][0] equals:[decoded delegate]];
|
||||
}
|
||||
|
||||
- (void)testADecodedSplitViewKeepsItsDelegate
|
||||
{
|
||||
var splitView = [[CPSplitView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
|
||||
|
||||
[splitView setDelegate:target];
|
||||
[container addSubview:splitView];
|
||||
|
||||
var decoded = [[self archived:container] subviews][1];
|
||||
|
||||
[self assert:[[decoded superview] subviews][0] equals:[decoded delegate]];
|
||||
}
|
||||
|
||||
// MARK: The view hierarchy
|
||||
|
||||
- (void)testADecodedSubviewKeepsItsResponderChain
|
||||
{
|
||||
var child = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[container addSubview:child];
|
||||
|
||||
var decoded = [self archived:container];
|
||||
|
||||
[self assert:decoded equals:[[decoded subviews][1] nextResponder]];
|
||||
}
|
||||
|
||||
- (void)testADecodedViewAutoresizesItsSubviews
|
||||
{
|
||||
var child = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
|
||||
|
||||
[child setAutoresizingMask:CPViewWidthSizable];
|
||||
[container addSubview:child];
|
||||
|
||||
var decoded = [self archived:container];
|
||||
|
||||
[decoded setFrameSize:CGSizeMake(400.0, 300.0)];
|
||||
|
||||
[self assert:200.0 equals:CGRectGetWidth([[decoded subviews][1] frame])];
|
||||
}
|
||||
|
||||
// MARK: Objects a class creates or observes
|
||||
|
||||
- (void)testADecodedButtonBarFollowsAHiddenButton
|
||||
{
|
||||
var buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 26.0)],
|
||||
buttons = [[CPButtonBar plusButton], [CPButtonBar minusButton]];
|
||||
|
||||
[buttonBar setButtons:buttons];
|
||||
[buttonBar layoutSubviews];
|
||||
|
||||
var decoded = [self archived:buttonBar],
|
||||
decodedButtons = [decoded buttons];
|
||||
|
||||
[decoded layoutSubviews];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
var firstOrigin = CGRectGetMinX([decodedButtons[0] frame]);
|
||||
|
||||
[decodedButtons[0] setHidden:YES];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assert:firstOrigin equals:CGRectGetMinX([decodedButtons[1] frame])];
|
||||
}
|
||||
|
||||
- (void)testADecodedStepperStepsItsValue
|
||||
{
|
||||
var stepper = [[CPStepper alloc] initWithFrame:CGRectMake(0.0, 0.0, 30.0, 25.0)];
|
||||
|
||||
[stepper setMinValue:1.0];
|
||||
[stepper setMaxValue:9.0];
|
||||
[stepper setDoubleValue:4.0];
|
||||
|
||||
var decoded = [self decodedConnectedControl:stepper];
|
||||
|
||||
[decoded performClickUp:nil];
|
||||
[self assert:5.0 equals:[decoded doubleValue]];
|
||||
|
||||
[decoded performClickDown:nil];
|
||||
[self assert:4.0 equals:[decoded doubleValue]];
|
||||
|
||||
[self assert:2 equals:[[decoded target] actionCount]];
|
||||
}
|
||||
|
||||
- (void)testADecodedSearchFieldCancelsItsText
|
||||
{
|
||||
var field = [[CPSearchField alloc] initWithFrame:CGRectMake(0.0, 0.0, 120.0, 30.0)];
|
||||
|
||||
[field setStringValue:@"Text"];
|
||||
|
||||
var decoded = [self archived:field];
|
||||
|
||||
[decoded cancelOperation:nil];
|
||||
|
||||
[self assert:@"" equals:[decoded stringValue]];
|
||||
[self assertTrue:[[decoded cancelButton] isHidden]];
|
||||
}
|
||||
|
||||
- (void)testADecodedPopUpButtonAcceptsNewItems
|
||||
{
|
||||
var button = [[CPPopUpButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 120.0, 24.0)];
|
||||
|
||||
[button addItemsWithTitles:[@"One", @"Two"]];
|
||||
[button selectItemAtIndex:1];
|
||||
|
||||
var decoded = [self archived:button];
|
||||
|
||||
[self assert:1 equals:[decoded indexOfSelectedItem]];
|
||||
[self assert:@"Two" equals:[decoded titleOfSelectedItem]];
|
||||
|
||||
[decoded addItemWithTitle:@"Three"];
|
||||
[decoded selectItemAtIndex:2];
|
||||
|
||||
[self assert:3 equals:[decoded numberOfItems]];
|
||||
[self assert:@"Three" equals:[decoded title]];
|
||||
}
|
||||
|
||||
- (void)testADecodedSegmentedControlSelectsASegment
|
||||
{
|
||||
var control = [[CPSegmentedControl alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 24.0)];
|
||||
|
||||
[control setSegmentCount:2];
|
||||
[control setLabel:@"One" forSegment:0];
|
||||
[control setLabel:@"Two" forSegment:1];
|
||||
[control setSelectedSegment:0];
|
||||
|
||||
var decoded = [self archived:control];
|
||||
|
||||
[self assert:0 equals:[decoded selectedSegment]];
|
||||
[self assert:@"Two" equals:[decoded labelForSegment:1]];
|
||||
|
||||
[decoded setSelectedSegment:1];
|
||||
|
||||
[self assert:1 equals:[decoded selectedSegment]];
|
||||
}
|
||||
|
||||
- (void)testADecodedTabViewSwitchesItsContent
|
||||
{
|
||||
var tabView = [[CPTabView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
|
||||
|
||||
[@"one", @"two"].forEach(function(anIdentifier)
|
||||
{
|
||||
var item = [[CPTabViewItem alloc] initWithIdentifier:anIdentifier];
|
||||
|
||||
[item setLabel:anIdentifier];
|
||||
[item setView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
|
||||
[tabView addTabViewItem:item];
|
||||
});
|
||||
|
||||
[tabView selectTabViewItemAtIndex:1];
|
||||
|
||||
var decoded = [self archived:tabView];
|
||||
|
||||
[self assert:@"two" equals:[[decoded selectedTabViewItem] identifier]];
|
||||
[self assertNotNull:[[[decoded selectedTabViewItem] view] superview]];
|
||||
|
||||
[decoded selectTabViewItemAtIndex:0];
|
||||
|
||||
[self assert:@"one" equals:[[decoded selectedTabViewItem] identifier]];
|
||||
[self assertNotNull:[[[decoded selectedTabViewItem] view] superview]];
|
||||
}
|
||||
|
||||
- (void)testADecodedSplitViewMovesItsDivider
|
||||
{
|
||||
var splitView = [[CPSplitView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 100.0)];
|
||||
|
||||
[splitView setVertical:YES];
|
||||
[splitView addSubview:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 95.0, 100.0)]];
|
||||
[splitView addSubview:[[CPView alloc] initWithFrame:CGRectMake(100.0, 0.0, 95.0, 100.0)]];
|
||||
|
||||
var decoded = [self archived:splitView];
|
||||
|
||||
[decoded setPosition:60.0 ofDividerAtIndex:0];
|
||||
|
||||
[self assert:60.0 equals:CGRectGetWidth([[decoded arrangedSubviews][0] frame])];
|
||||
}
|
||||
|
||||
- (void)testADecodedScrollViewScrollsItsDocument
|
||||
{
|
||||
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
|
||||
|
||||
[scrollView setDocumentView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)]];
|
||||
|
||||
var decoded = [self archived:scrollView];
|
||||
|
||||
[[decoded documentView] scrollPoint:CGPointMake(0.0, 50.0)];
|
||||
|
||||
[self assert:50.0 equals:CGRectGetMinY([decoded documentVisibleRect])];
|
||||
}
|
||||
|
||||
- (void)testADecodedBoxAcceptsANewContentView
|
||||
{
|
||||
var box = [[CPBox alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 100.0)];
|
||||
|
||||
[box setContentView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
|
||||
|
||||
var decoded = [self archived:box],
|
||||
newContentView = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 60.0, 60.0)];
|
||||
|
||||
[decoded setContentView:newContentView];
|
||||
|
||||
[self assert:newContentView equals:[decoded contentView]];
|
||||
[self assertNotNull:[newContentView superview]];
|
||||
}
|
||||
|
||||
- (void)testADecodedAccordionKeepsACollapsedItem
|
||||
{
|
||||
var accordion = [[CPAccordionView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)],
|
||||
item = [[CPAccordionViewItem alloc] initWithIdentifier:@"item"];
|
||||
|
||||
[item setLabel:@"Item"];
|
||||
[item setView:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 50.0)]];
|
||||
[accordion addItem:item];
|
||||
[accordion collapseItemAtIndex:0];
|
||||
|
||||
var decoded = [self archived:accordion];
|
||||
|
||||
[self assertFalse:[[decoded expandedItemIndexes] containsIndex:0]];
|
||||
|
||||
[decoded toggleItemAtIndex:0];
|
||||
|
||||
[self assertTrue:[[decoded expandedItemIndexes] containsIndex:0]];
|
||||
}
|
||||
|
||||
- (void)testADecodedStackViewAcceptsAnArrangedSubview
|
||||
{
|
||||
var stackView = [[CPStackView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
|
||||
|
||||
[stackView addArrangedSubview:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
|
||||
|
||||
var decoded = [self archived:stackView];
|
||||
|
||||
[decoded addArrangedSubview:[[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
|
||||
|
||||
[self assert:2 equals:[[decoded arrangedSubviews] count]];
|
||||
}
|
||||
|
||||
- (void)testADecodedTokenFieldEndsWithItsTokens
|
||||
{
|
||||
var field = [[CPTokenField alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 30.0)];
|
||||
|
||||
[field setObjectValue:[@"One", @"Two"]];
|
||||
|
||||
var decoded = [self archived:field];
|
||||
|
||||
[self assert:[@"One", @"Two"] equals:[decoded objectValue]];
|
||||
|
||||
[decoded setObjectValue:[@"Two"]];
|
||||
|
||||
[self assert:[@"Two"] equals:[decoded objectValue]];
|
||||
}
|
||||
|
||||
- (void)testADecodedTableReadsItsDataSource
|
||||
{
|
||||
var tableView = [[CPTableView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)];
|
||||
|
||||
[tableView addTableColumn:[[CPTableColumn alloc] initWithIdentifier:@"Column"]];
|
||||
[tableView setDataSource:[[_CPDecodedTableDataSource alloc] init]];
|
||||
|
||||
var decoded = [self archived:tableView];
|
||||
|
||||
[decoded reloadData];
|
||||
|
||||
[self assert:3 equals:[decoded numberOfRows]];
|
||||
}
|
||||
|
||||
- (void)testADecodedOutlineReadsItsDataSource
|
||||
{
|
||||
var outlineView = [[CPOutlineView alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 200.0)],
|
||||
column = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
|
||||
|
||||
[outlineView addTableColumn:column];
|
||||
[outlineView setOutlineTableColumn:column];
|
||||
[outlineView setDataSource:[[_CPDecodedOutlineDataSource alloc] init]];
|
||||
|
||||
var decoded = [self archived:outlineView];
|
||||
|
||||
[decoded reloadData];
|
||||
|
||||
[self assert:1 equals:[decoded numberOfRows]];
|
||||
[self assert:@"Parent" equals:[decoded itemAtRow:0]];
|
||||
[self assert:@"Parent" equals:[decoded _objectValueForTableColumn:[decoded outlineTableColumn] row:0 useCache:NO]];
|
||||
|
||||
[decoded expandItem:@"Parent"];
|
||||
|
||||
[self assert:2 equals:[decoded numberOfRows]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* CPViewThemeChangeTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
A view resolves each theme attribute from its own values, then its theme, then its
|
||||
class default. When any of these changes, the view must resolve the new value and
|
||||
lay itself out again. These tests build small themes in code, so they depend on no
|
||||
real theme.
|
||||
*/
|
||||
|
||||
@implementation _CPThemeChangeView : CPView
|
||||
{
|
||||
int layoutCount @accessors;
|
||||
int themeChangeCount @accessors;
|
||||
int displayRequestCount @accessors;
|
||||
}
|
||||
|
||||
+ (CPString)defaultThemeClass
|
||||
{
|
||||
return @"theme-change-view";
|
||||
}
|
||||
|
||||
+ (CPDictionary)themeAttributes
|
||||
{
|
||||
return @{
|
||||
@"accent": @"class default",
|
||||
@"margin": @"class default"
|
||||
};
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
layoutCount++;
|
||||
}
|
||||
|
||||
/*
|
||||
needsDisplay is true only for a view with its own drawRect:, so the request is
|
||||
recorded here.
|
||||
*/
|
||||
- (void)setNeedsDisplay:(BOOL)aFlag
|
||||
{
|
||||
if (aFlag)
|
||||
displayRequestCount++;
|
||||
|
||||
[super setNeedsDisplay:aFlag];
|
||||
}
|
||||
|
||||
- (void)objectDidChangeTheme
|
||||
{
|
||||
themeChangeCount++;
|
||||
|
||||
[super objectDidChangeTheme];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPViewThemeChangeTest : OJTestCase
|
||||
{
|
||||
CPTheme firstTheme;
|
||||
CPTheme secondTheme;
|
||||
CPTheme defaultTheme;
|
||||
_CPThemeChangeView view;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
defaultTheme = [CPTheme defaultTheme];
|
||||
firstTheme = [self themeNamed:@"theme-change-first" themeClass:nil accent:@"first"];
|
||||
secondTheme = [self themeNamed:@"theme-change-second" themeClass:nil accent:@"second"];
|
||||
view = [self newView];
|
||||
|
||||
[view setTheme:firstTheme];
|
||||
[_CPDisplayServer run];
|
||||
[view setLayoutCount:0];
|
||||
[view setThemeChangeCount:0];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
[CPTheme setDefaultTheme:defaultTheme];
|
||||
}
|
||||
|
||||
- (_CPThemeChangeView)newView
|
||||
{
|
||||
return [[_CPThemeChangeView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
|
||||
}
|
||||
|
||||
- (CPTheme)themeNamed:(CPString)aName themeClass:(CPString)aThemeClass accent:(CPString)anAccent
|
||||
{
|
||||
var theme = [[CPTheme alloc] initWithName:aName],
|
||||
template = [self newView];
|
||||
|
||||
if (aThemeClass)
|
||||
[template setThemeClass:aThemeClass];
|
||||
|
||||
[template setValue:anAccent forThemeAttribute:@"accent"];
|
||||
[theme takeThemeFromObject:template];
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
- (id)archived:(CPView)aView
|
||||
{
|
||||
return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:aView]];
|
||||
}
|
||||
|
||||
- (void)assertAccent:(CPString)aValue ofView:(CPView)aView
|
||||
{
|
||||
[self assert:aValue equals:[aView currentValueForThemeAttribute:@"accent"]];
|
||||
}
|
||||
|
||||
- (void)testAViewResolvesItsThemeValue
|
||||
{
|
||||
[self assertAccent:@"first" ofView:view];
|
||||
}
|
||||
|
||||
- (void)testAnAttributeTheThemeDoesNotDefineUsesTheClassDefault
|
||||
{
|
||||
[self assert:@"class default" equals:[view currentValueForThemeAttribute:@"margin"]];
|
||||
}
|
||||
|
||||
- (void)testAViewWithoutAThemeUsesTheClassDefault
|
||||
{
|
||||
[view setTheme:nil];
|
||||
|
||||
[self assertAccent:@"class default" ofView:view];
|
||||
}
|
||||
|
||||
- (void)testSettingAThemeChangesTheResolvedValue
|
||||
{
|
||||
[view setTheme:secondTheme];
|
||||
|
||||
[self assertAccent:@"second" ofView:view];
|
||||
}
|
||||
|
||||
- (void)testSettingAThemeNotifiesTheView
|
||||
{
|
||||
[view setTheme:secondTheme];
|
||||
|
||||
[self assert:1 equals:[view themeChangeCount]];
|
||||
}
|
||||
|
||||
- (void)testSettingTheSameThemeDoesNothing
|
||||
{
|
||||
[view setTheme:firstTheme];
|
||||
|
||||
[self assert:0 equals:[view themeChangeCount]];
|
||||
}
|
||||
|
||||
- (void)testSettingAThemeQueuesLayout
|
||||
{
|
||||
[view setTheme:secondTheme];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assert:1 equals:[view layoutCount]];
|
||||
}
|
||||
|
||||
- (void)testSettingAThemeRequestsDisplay
|
||||
{
|
||||
[view setDisplayRequestCount:0];
|
||||
[view setTheme:secondTheme];
|
||||
|
||||
[self assertTrue:[view displayRequestCount] > 0];
|
||||
}
|
||||
|
||||
- (void)testAThemeChangeReachesNestedViews
|
||||
{
|
||||
var parent = [self newView],
|
||||
child = [self newView],
|
||||
grandchild = [self newView];
|
||||
|
||||
[parent addSubview:child];
|
||||
[child addSubview:grandchild];
|
||||
[parent _setThemeIncludingDescendants:secondTheme];
|
||||
|
||||
[self assertAccent:@"second" ofView:parent];
|
||||
[self assertAccent:@"second" ofView:child];
|
||||
[self assertAccent:@"second" ofView:grandchild];
|
||||
}
|
||||
|
||||
- (void)testAPerViewValueTakesPriorityOverTheTheme
|
||||
{
|
||||
[view setValue:@"own" forThemeAttribute:@"accent"];
|
||||
|
||||
[self assertAccent:@"own" ofView:view];
|
||||
}
|
||||
|
||||
- (void)testAPerViewValueSurvivesAThemeChange
|
||||
{
|
||||
[view setValue:@"own" forThemeAttribute:@"accent"];
|
||||
[view setTheme:secondTheme];
|
||||
|
||||
[self assertAccent:@"own" ofView:view];
|
||||
}
|
||||
|
||||
- (void)testAPerViewValueQueuesLayout
|
||||
{
|
||||
[view setValue:@"own" forThemeAttribute:@"accent"];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assert:1 equals:[view layoutCount]];
|
||||
}
|
||||
|
||||
- (void)testSettingTheSamePerViewValueQueuesNoLayout
|
||||
{
|
||||
[view setValue:@"first" forThemeAttribute:@"accent"];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assert:0 equals:[view layoutCount]];
|
||||
}
|
||||
|
||||
- (void)testAStateSelectsItsValue
|
||||
{
|
||||
[view setValue:@"disabled" forThemeAttribute:@"accent" inState:CPThemeStateDisabled];
|
||||
[self assertAccent:@"first" ofView:view];
|
||||
|
||||
[view setThemeState:CPThemeStateDisabled];
|
||||
[self assertAccent:@"disabled" ofView:view];
|
||||
|
||||
[view unsetThemeState:CPThemeStateDisabled];
|
||||
[self assertAccent:@"first" ofView:view];
|
||||
}
|
||||
|
||||
- (void)testAStateChangeQueuesLayout
|
||||
{
|
||||
[view setThemeState:CPThemeStateDisabled];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assert:1 equals:[view layoutCount]];
|
||||
}
|
||||
|
||||
- (void)testSettingAThemeClassChangesTheResolvedValue
|
||||
{
|
||||
var theme = [self themeNamed:@"theme-change-class" themeClass:@"other-class" accent:@"other class"];
|
||||
|
||||
[view setTheme:theme];
|
||||
[view setThemeClass:@"other-class"];
|
||||
|
||||
[self assertAccent:@"other class" ofView:view];
|
||||
}
|
||||
|
||||
- (void)testSettingAThemeClassQueuesLayout
|
||||
{
|
||||
[view setThemeClass:@"other-class"];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assert:1 equals:[view layoutCount]];
|
||||
}
|
||||
|
||||
- (void)testANewViewUsesTheDefaultTheme
|
||||
{
|
||||
[CPTheme setDefaultTheme:secondTheme];
|
||||
|
||||
[self assertAccent:@"second" ofView:[self newView]];
|
||||
}
|
||||
|
||||
- (void)testChangingTheDefaultThemeLeavesAnExistingViewUnchanged
|
||||
{
|
||||
[CPTheme setDefaultTheme:secondTheme];
|
||||
|
||||
[self assertAccent:@"first" ofView:view];
|
||||
}
|
||||
|
||||
- (void)testADecodedViewUsesTheDefaultTheme
|
||||
{
|
||||
[CPTheme setDefaultTheme:secondTheme];
|
||||
|
||||
[self assertAccent:@"second" ofView:[self archived:view]];
|
||||
}
|
||||
|
||||
- (void)testADecodedViewKeepsAPerViewValue
|
||||
{
|
||||
[view setValue:@"own" forThemeAttribute:@"accent"];
|
||||
|
||||
[self assertAccent:@"own" ofView:[self archived:view]];
|
||||
}
|
||||
|
||||
- (void)testADecodedViewKeepsAPerViewStateValue
|
||||
{
|
||||
[view setValue:@"disabled" forThemeAttribute:@"accent" inState:CPThemeStateDisabled];
|
||||
|
||||
var decoded = [self archived:view];
|
||||
|
||||
[decoded setThemeState:CPThemeStateDisabled];
|
||||
|
||||
[self assertAccent:@"disabled" ofView:decoded];
|
||||
}
|
||||
|
||||
- (void)testADecodedViewKeepsItsThemeState
|
||||
{
|
||||
[view setThemeState:CPThemeStateDisabled];
|
||||
|
||||
[self assertTrue:[[self archived:view] hasThemeState:CPThemeStateDisabled]];
|
||||
}
|
||||
|
||||
- (void)testADecodedViewKeepsItsThemeClass
|
||||
{
|
||||
[view setThemeClass:@"other-class"];
|
||||
|
||||
[self assert:@"other-class" equals:[[self archived:view] themeClass]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
* CPViewThemeStateTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
A theme value is chosen by the state of a view, so a view with the wrong state
|
||||
draws wrongly with a correct theme. These tests check that the states that depend
|
||||
on a view's surroundings stay correct as views enter and leave windows and as
|
||||
windows change. The states are the key window state, the first responder state
|
||||
and the appearance states. Only a window's own window view has the main window
|
||||
state.
|
||||
|
||||
These tests run with no theme loaded. They check the states, and not what a theme
|
||||
does with them.
|
||||
*/
|
||||
|
||||
@implementation _CPThemeStateRecordingView : CPView
|
||||
{
|
||||
CPArray calls @accessors;
|
||||
BOOL hadKeyStateOnArrival @accessors;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
self = [super initWithFrame:aFrame];
|
||||
|
||||
if (self)
|
||||
calls = [];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)viewWillMoveToWindow:(CPWindow)aWindow
|
||||
{
|
||||
calls.push({ @"call": @"willMove", @"from": [self window], @"to": aWindow });
|
||||
}
|
||||
|
||||
- (void)viewDidMoveToWindow
|
||||
{
|
||||
hadKeyStateOnArrival = [self hasThemeState:CPThemeStateKeyWindow];
|
||||
calls.push({ @"call": @"didMove", @"window": [self window] });
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPViewThemeStateTest : OJTestCase
|
||||
{
|
||||
CPWindow firstWindow;
|
||||
CPWindow secondWindow;
|
||||
_CPThemeStateRecordingView parentView;
|
||||
_CPThemeStateRecordingView childView;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
firstWindow = [self newWindow];
|
||||
secondWindow = [self newWindow];
|
||||
|
||||
parentView = [[_CPThemeStateRecordingView alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)];
|
||||
childView = [[_CPThemeStateRecordingView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)];
|
||||
|
||||
[parentView addSubview:childView];
|
||||
}
|
||||
|
||||
- (CPWindow)newWindow
|
||||
{
|
||||
return [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 200.0, 200.0) styleMask:CPTitledWindowMask];
|
||||
}
|
||||
|
||||
- (void)assertKeyState:(BOOL)shouldHaveState
|
||||
{
|
||||
[self assert:shouldHaveState equals:[parentView hasThemeState:CPThemeStateKeyWindow] message:@"parent view"];
|
||||
[self assert:shouldHaveState equals:[childView hasThemeState:CPThemeStateKeyWindow] message:@"child view"];
|
||||
}
|
||||
|
||||
- (void)assertFirstResponderState:(BOOL)shouldHaveState
|
||||
{
|
||||
[self assert:shouldHaveState equals:[parentView hasThemeState:CPThemeStateFirstResponder] message:@"parent view"];
|
||||
[self assert:shouldHaveState equals:[childView hasThemeState:CPThemeStateFirstResponder] message:@"child view"];
|
||||
}
|
||||
|
||||
- (void)clearCalls
|
||||
{
|
||||
[parentView setCalls:[]];
|
||||
[childView setCalls:[]];
|
||||
}
|
||||
|
||||
// MARK: Key window state
|
||||
|
||||
- (void)testAViewOutsideAWindowHasNoKeyState
|
||||
{
|
||||
[self assertKeyState:NO];
|
||||
}
|
||||
|
||||
- (void)testBecomingKeyReachesNestedViews
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[self assertKeyState:NO];
|
||||
|
||||
[firstWindow makeKeyWindow];
|
||||
[self assertKeyState:YES];
|
||||
}
|
||||
|
||||
- (void)testResigningKeyReachesNestedViews
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeKeyWindow];
|
||||
[firstWindow resignKeyWindow];
|
||||
|
||||
[self assertKeyState:NO];
|
||||
}
|
||||
|
||||
- (void)testAnotherWindowBecomingKeyClearsTheState
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeKeyWindow];
|
||||
[secondWindow makeKeyWindow];
|
||||
|
||||
[self assertKeyState:NO];
|
||||
}
|
||||
|
||||
- (void)testAViewAddedToAKeyWindowGainsTheState
|
||||
{
|
||||
[firstWindow makeKeyWindow];
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
|
||||
[self assertKeyState:YES];
|
||||
}
|
||||
|
||||
- (void)testAViewAddedBelowAViewInAKeyWindowGainsTheState
|
||||
{
|
||||
var lateView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeKeyWindow];
|
||||
[childView addSubview:lateView];
|
||||
|
||||
[self assertTrue:[lateView hasThemeState:CPThemeStateKeyWindow]];
|
||||
}
|
||||
|
||||
- (void)testAViewRemovedFromAKeyWindowLosesTheState
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeKeyWindow];
|
||||
[parentView removeFromSuperview];
|
||||
|
||||
[self assertKeyState:NO];
|
||||
}
|
||||
|
||||
- (void)testMovingToANonKeyWindowClearsTheState
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeKeyWindow];
|
||||
[[secondWindow contentView] addSubview:parentView];
|
||||
|
||||
[self assertKeyState:NO];
|
||||
}
|
||||
|
||||
- (void)testMovingToAKeyWindowSetsTheState
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[secondWindow makeKeyWindow];
|
||||
[[secondWindow contentView] addSubview:parentView];
|
||||
|
||||
[self assertKeyState:YES];
|
||||
}
|
||||
|
||||
- (void)testANewContentViewGainsTheState
|
||||
{
|
||||
[firstWindow makeKeyWindow];
|
||||
[firstWindow setContentView:parentView];
|
||||
|
||||
[self assertKeyState:YES];
|
||||
}
|
||||
|
||||
- (void)testTheStateIsCorrectInViewDidMoveToWindow
|
||||
{
|
||||
[firstWindow makeKeyWindow];
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
|
||||
[self assertTrue:[parentView hadKeyStateOnArrival]];
|
||||
[self assertTrue:[childView hadKeyStateOnArrival]];
|
||||
}
|
||||
|
||||
// MARK: Window view
|
||||
|
||||
/*
|
||||
A window must be visible to become main, and ordering a window front needs a DOM.
|
||||
So this test sends becomeMainWindow directly, and it does not cover makeMainWindow.
|
||||
*/
|
||||
- (void)testTheWindowViewFollowsKeyAndMainStatus
|
||||
{
|
||||
var windowView = [firstWindow _windowView];
|
||||
|
||||
[firstWindow makeKeyWindow];
|
||||
[firstWindow becomeMainWindow];
|
||||
|
||||
[self assertTrue:[windowView hasThemeState:CPThemeStateKeyWindow]];
|
||||
[self assertTrue:[windowView hasThemeState:CPThemeStateMainWindow]];
|
||||
|
||||
[firstWindow resignKeyWindow];
|
||||
[firstWindow resignMainWindow];
|
||||
|
||||
[self assertFalse:[windowView hasThemeState:CPThemeStateKeyWindow]];
|
||||
[self assertFalse:[windowView hasThemeState:CPThemeStateMainWindow]];
|
||||
}
|
||||
|
||||
// MARK: First responder state
|
||||
|
||||
- (void)testBecomingFirstResponderReachesNestedViews
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeFirstResponder:parentView];
|
||||
|
||||
[self assertFirstResponderState:YES];
|
||||
}
|
||||
|
||||
- (void)testResigningFirstResponderReachesNestedViews
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeFirstResponder:parentView];
|
||||
[firstWindow makeFirstResponder:nil];
|
||||
|
||||
[self assertFirstResponderState:NO];
|
||||
}
|
||||
|
||||
- (void)testAViewAddedBelowTheFirstResponderGainsTheState
|
||||
{
|
||||
var lateView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeFirstResponder:parentView];
|
||||
[childView addSubview:lateView];
|
||||
|
||||
[self assertTrue:[lateView hasThemeState:CPThemeStateFirstResponder]];
|
||||
}
|
||||
|
||||
- (void)testRemovingTheFirstResponderClearsTheState
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[firstWindow makeFirstResponder:parentView];
|
||||
[parentView removeFromSuperview];
|
||||
|
||||
[self assertFirstResponderState:NO];
|
||||
[self assertFalse:[firstWindow firstResponder] === parentView];
|
||||
}
|
||||
|
||||
// MARK: Window notifications
|
||||
|
||||
- (void)testEnteringAWindowNotifiesEachViewInOrder
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
|
||||
[parentView, childView].forEach(function(aView)
|
||||
{
|
||||
var calls = [aView calls];
|
||||
|
||||
[self assert:2 equals:calls.length];
|
||||
[self assert:@"willMove" equals:calls[0].call];
|
||||
[self assert:firstWindow equals:calls[0].to];
|
||||
[self assertNull:calls[0].from];
|
||||
[self assert:@"didMove" equals:calls[1].call];
|
||||
[self assert:firstWindow equals:calls[1].window];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testLeavingAWindowNotifiesEachView
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[self clearCalls];
|
||||
[parentView removeFromSuperview];
|
||||
|
||||
[parentView, childView].forEach(function(aView)
|
||||
{
|
||||
var calls = [aView calls];
|
||||
|
||||
[self assert:2 equals:calls.length];
|
||||
[self assertNull:calls[0].to];
|
||||
[self assert:firstWindow equals:calls[0].from];
|
||||
[self assertNull:calls[1].window];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testMovingBetweenWindowsNotifiesEachView
|
||||
{
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[self clearCalls];
|
||||
[[secondWindow contentView] addSubview:parentView];
|
||||
|
||||
[parentView, childView].forEach(function(aView)
|
||||
{
|
||||
var calls = [aView calls];
|
||||
|
||||
[self assert:2 equals:calls.length];
|
||||
[self assert:firstWindow equals:calls[0].from];
|
||||
[self assert:secondWindow equals:calls[0].to];
|
||||
[self assert:secondWindow equals:calls[1].window];
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Cocoa also sends these notifications when a view stays in its window and only its
|
||||
position in the view hierarchy changes.
|
||||
*/
|
||||
- (void)testMovingWithinAWindowNotifiesEachView
|
||||
{
|
||||
var otherView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[[firstWindow contentView] addSubview:parentView];
|
||||
[[firstWindow contentView] addSubview:otherView];
|
||||
[self clearCalls];
|
||||
[otherView addSubview:parentView];
|
||||
|
||||
[parentView, childView].forEach(function(aView)
|
||||
{
|
||||
var calls = [aView calls];
|
||||
|
||||
[self assert:2 equals:calls.length];
|
||||
[self assert:@"willMove" equals:calls[0].call];
|
||||
[self assert:firstWindow equals:calls[0].to];
|
||||
[self assert:@"didMove" equals:calls[1].call];
|
||||
[self assert:firstWindow equals:calls[1].window];
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: Appearance states
|
||||
|
||||
- (void)testEffectiveAppearanceComesFromTheNearestAncestor
|
||||
{
|
||||
var appearance = [CPAppearance appearanceNamed:CPAppearanceNameVibrantDark];
|
||||
|
||||
[parentView setAppearance:appearance];
|
||||
|
||||
[self assert:appearance equals:[childView effectiveAppearance]];
|
||||
}
|
||||
|
||||
- (void)testOwnAppearanceOverridesAnAncestor
|
||||
{
|
||||
var appearance = [CPAppearance appearanceNamed:CPAppearanceNameVibrantLight];
|
||||
|
||||
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]];
|
||||
[childView setAppearance:appearance];
|
||||
|
||||
[self assert:appearance equals:[childView effectiveAppearance]];
|
||||
}
|
||||
|
||||
- (void)testAppearanceStateReachesNestedViews
|
||||
{
|
||||
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assertTrue:[parentView hasThemeState:CPThemeStateAppearanceVibrantDark]];
|
||||
[self assertTrue:[childView hasThemeState:CPThemeStateAppearanceVibrantDark]];
|
||||
}
|
||||
|
||||
- (void)testChangingAppearanceReplacesTheState
|
||||
{
|
||||
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]];
|
||||
[_CPDisplayServer run];
|
||||
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantLight]];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assertFalse:[childView hasThemeState:CPThemeStateAppearanceVibrantDark]];
|
||||
[self assertTrue:[childView hasThemeState:CPThemeStateAppearanceVibrantLight]];
|
||||
}
|
||||
|
||||
- (void)testAViewAddedBelowAnAppearanceGainsItsState
|
||||
{
|
||||
var lateView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
[parentView setAppearance:[CPAppearance appearanceNamed:CPAppearanceNameVibrantDark]];
|
||||
[_CPDisplayServer run];
|
||||
[childView addSubview:lateView];
|
||||
[lateView setNeedsLayout];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
[self assertTrue:[lateView hasThemeState:CPThemeStateAppearanceVibrantDark]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* DropInArchiveFixtures.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "DropInSnapshot.j"
|
||||
|
||||
/*
|
||||
Creates the views that the archive tests write and read back. An empty view has
|
||||
little to lose, so each view holds real content. The reference for a view is its
|
||||
own description before it is archived, never what a build restores.
|
||||
*/
|
||||
|
||||
/*
|
||||
The only view classes without a fixture. Each entry states the reason. Any other
|
||||
class that cannot be archived is a failure.
|
||||
*/
|
||||
var DropInArchiveExclusions = {
|
||||
@"CPText": @"it is abstract and its text methods raise an exception",
|
||||
@"CPWebView": @"creating one needs a DOM"
|
||||
};
|
||||
|
||||
function DropInArchiveExclusion(aClassName)
|
||||
{
|
||||
return DropInArchiveExclusions[aClassName];
|
||||
}
|
||||
|
||||
function DropInArchiveChild(aWidth, aHeight)
|
||||
{
|
||||
return [[CPView alloc] initWithFrame:CGRectMake(3.0, 5.0, aWidth, aHeight)];
|
||||
}
|
||||
|
||||
function DropInArchiveTableColumn(anIdentifier)
|
||||
{
|
||||
var column = [[CPTableColumn alloc] initWithIdentifier:anIdentifier];
|
||||
|
||||
[column setWidth:60.0];
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
/*
|
||||
The content of each class. A class without an entry uses the entry of its
|
||||
nearest superclass.
|
||||
*/
|
||||
var DropInArchivePopulators = {
|
||||
@"CPView": function(aView)
|
||||
{
|
||||
[aView addSubview:DropInArchiveChild(20.0, 10.0)];
|
||||
},
|
||||
@"CPControl": function(aControl)
|
||||
{
|
||||
[aControl setEnabled:NO];
|
||||
[aControl setTag:7];
|
||||
},
|
||||
@"CPAccordionView": function(anAccordionView)
|
||||
{
|
||||
var item = [[CPAccordionViewItem alloc] initWithIdentifier:@"item"];
|
||||
|
||||
[item setLabel:@"Item"];
|
||||
[item setView:DropInArchiveChild(40.0, 30.0)];
|
||||
[anAccordionView addItem:item];
|
||||
},
|
||||
@"CPBox": function(aBox)
|
||||
{
|
||||
var content = DropInArchiveChild(60.0, 40.0);
|
||||
|
||||
[content addSubview:DropInArchiveChild(20.0, 10.0)];
|
||||
[aBox setTitle:@"Box"];
|
||||
[aBox setTitlePosition:CPAtTop];
|
||||
[aBox setContentView:content];
|
||||
},
|
||||
@"CPButton": function(aButton)
|
||||
{
|
||||
[aButton setTitle:@"Button"];
|
||||
[aButton setState:CPOnState];
|
||||
[aButton setTag:7];
|
||||
},
|
||||
@"CPButtonBar": function(aButtonBar)
|
||||
{
|
||||
[aButtonBar setButtons:[[CPButtonBar plusButton], [CPButtonBar minusButton]]];
|
||||
},
|
||||
@"CPColorWell": function(aColorWell)
|
||||
{
|
||||
[aColorWell setColor:[CPColor redColor]];
|
||||
},
|
||||
@"CPComboBox": function(aComboBox)
|
||||
{
|
||||
[aComboBox addItemsWithObjectValues:[@"One", @"Two"]];
|
||||
[aComboBox setStringValue:@"One"];
|
||||
},
|
||||
@"CPDatePicker": function(aDatePicker)
|
||||
{
|
||||
[aDatePicker setDateValue:[[CPDate alloc] initWithString:@"2026-10-31 12:00:00 +0000"]];
|
||||
},
|
||||
@"CPLevelIndicator": function(aLevelIndicator)
|
||||
{
|
||||
[aLevelIndicator setMinValue:1.0];
|
||||
[aLevelIndicator setMaxValue:9.0];
|
||||
[aLevelIndicator setDoubleValue:4.0];
|
||||
},
|
||||
@"CPOutlineView": function(anOutlineView)
|
||||
{
|
||||
var column = DropInArchiveTableColumn(@"one");
|
||||
|
||||
[anOutlineView addTableColumn:column];
|
||||
[anOutlineView setOutlineTableColumn:column];
|
||||
},
|
||||
@"CPPopUpButton": function(aPopUpButton)
|
||||
{
|
||||
[aPopUpButton addItemsWithTitles:[@"One", @"Two"]];
|
||||
[aPopUpButton selectItemAtIndex:1];
|
||||
},
|
||||
@"CPProgressIndicator": function(aProgressIndicator)
|
||||
{
|
||||
[aProgressIndicator setMinValue:1.0];
|
||||
[aProgressIndicator setMaxValue:9.0];
|
||||
[aProgressIndicator setDoubleValue:4.0];
|
||||
},
|
||||
@"CPScrollView": function(aScrollView)
|
||||
{
|
||||
[aScrollView setDocumentView:DropInArchiveChild(300.0, 400.0)];
|
||||
},
|
||||
@"CPSegmentedControl": function(aSegmentedControl)
|
||||
{
|
||||
[aSegmentedControl setSegmentCount:2];
|
||||
[aSegmentedControl setLabel:@"One" forSegment:0];
|
||||
[aSegmentedControl setLabel:@"Two" forSegment:1];
|
||||
[aSegmentedControl setWidth:40.0 forSegment:0];
|
||||
[aSegmentedControl setWidth:40.0 forSegment:1];
|
||||
},
|
||||
@"CPSlider": function(aSlider)
|
||||
{
|
||||
[aSlider setMinValue:1.0];
|
||||
[aSlider setMaxValue:9.0];
|
||||
[aSlider setDoubleValue:4.0];
|
||||
},
|
||||
@"CPSplitView": function(aSplitView)
|
||||
{
|
||||
[aSplitView addSubview:DropInArchiveChild(40.0, 90.0)];
|
||||
[aSplitView addSubview:DropInArchiveChild(40.0, 90.0)];
|
||||
},
|
||||
@"CPStackView": function(aStackView)
|
||||
{
|
||||
[aStackView addArrangedSubview:DropInArchiveChild(40.0, 30.0)];
|
||||
},
|
||||
@"CPStepper": function(aStepper)
|
||||
{
|
||||
[aStepper setMinValue:1.0];
|
||||
[aStepper setMaxValue:9.0];
|
||||
[aStepper setDoubleValue:4.0];
|
||||
},
|
||||
@"CPTableView": function(aTableView)
|
||||
{
|
||||
[aTableView addTableColumn:DropInArchiveTableColumn(@"one")];
|
||||
[aTableView addTableColumn:DropInArchiveTableColumn(@"two")];
|
||||
},
|
||||
@"CPTabView": function(aTabView)
|
||||
{
|
||||
["one", "two"].forEach(function(anIdentifier)
|
||||
{
|
||||
var item = [[CPTabViewItem alloc] initWithIdentifier:anIdentifier];
|
||||
|
||||
[item setLabel:anIdentifier];
|
||||
[item setView:DropInArchiveChild(40.0, 30.0)];
|
||||
[aTabView addTabViewItem:item];
|
||||
});
|
||||
|
||||
[aTabView selectTabViewItemAtIndex:1];
|
||||
},
|
||||
@"CPTextView": function(aTextView)
|
||||
{
|
||||
[aTextView setString:@"Text"];
|
||||
},
|
||||
@"CPTextField": function(aTextField)
|
||||
{
|
||||
[aTextField setStringValue:@"Text"];
|
||||
[aTextField setEditable:YES];
|
||||
[aTextField setBezeled:YES];
|
||||
},
|
||||
@"CPTokenField": function(aTokenField)
|
||||
{
|
||||
[aTokenField setObjectValue:[@"One", @"Two"]];
|
||||
}
|
||||
};
|
||||
|
||||
function DropInArchivePopulator(aClass)
|
||||
{
|
||||
for (; aClass; aClass = class_getSuperclass(aClass))
|
||||
if (DropInArchivePopulators[aClass.name])
|
||||
return DropInArchivePopulators[aClass.name];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
function DropInArchiveFixture(aClass)
|
||||
{
|
||||
var view = [[aClass alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 100.0)];
|
||||
|
||||
DropInArchivePopulator(aClass)(view);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/*
|
||||
Lists each place where two view descriptions differ.
|
||||
*/
|
||||
function DropInTreeDifferences(expected, current, aPath, differences)
|
||||
{
|
||||
if (expected && current && typeof expected === "object" && typeof current === "object")
|
||||
{
|
||||
Object.keys(expected).concat(Object.keys(current)).filter(function(aKey, anIndex, aList)
|
||||
{
|
||||
return aList.indexOf(aKey) === anIndex;
|
||||
}).forEach(function(aKey)
|
||||
{
|
||||
DropInTreeDifferences(expected[aKey], current[aKey], aPath + "." + aKey, differences);
|
||||
});
|
||||
}
|
||||
else if (expected !== current)
|
||||
differences.push(aPath + ": " + JSON.stringify(expected) + " is now " + JSON.stringify(current));
|
||||
|
||||
return differences;
|
||||
}
|
||||
|
||||
/*
|
||||
Layout changes frames and adds subviews, and a new view has not asked for it. Every
|
||||
view is laid out before it is described, so that two descriptions are comparable.
|
||||
*/
|
||||
function DropInArchiveLayOut(aView)
|
||||
{
|
||||
[aView setNeedsLayout];
|
||||
[_CPDisplayServer run];
|
||||
}
|
||||
|
||||
/*
|
||||
Archives a fixture and reads it back.
|
||||
*/
|
||||
function DropInArchiveRoundTrip(aClass)
|
||||
{
|
||||
return DropInWithFixedScrollerStyle(function()
|
||||
{
|
||||
var view = DropInArchiveFixture(aClass);
|
||||
|
||||
DropInArchiveLayOut(view);
|
||||
|
||||
var original = DropInViewTree(view),
|
||||
data = [CPKeyedArchiver archivedDataWithRootObject:view],
|
||||
decoded = [CPKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
|
||||
DropInArchiveLayOut(decoded);
|
||||
|
||||
return { @"archive": [data rawString], @"original": original, @"decoded": DropInViewTree(decoded) };
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Reads a saved archive back and describes the view.
|
||||
*/
|
||||
function DropInSavedArchiveTree(aRawString)
|
||||
{
|
||||
return DropInWithFixedScrollerStyle(function()
|
||||
{
|
||||
var decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:aRawString]];
|
||||
|
||||
DropInArchiveLayOut(decoded);
|
||||
|
||||
return DropInViewTree(decoded);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* DropInArchiveGenerator.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "DropInArchiveFixtures.j"
|
||||
|
||||
/*
|
||||
Saves the archive test data. Run this only against a build of main:
|
||||
|
||||
objj Tests/AppKit/DropInArchiveGenerator.j
|
||||
|
||||
It saves a description of what each reference cib produces. It then saves an
|
||||
archive of each view fixture, with the fixture's description before archiving.
|
||||
An archive is saved only if it restores that description, so a failure stops
|
||||
the generator and no archive data is written.
|
||||
*/
|
||||
var fs = require("fs"),
|
||||
path = require("path");
|
||||
|
||||
function main(args, namedArgs)
|
||||
{
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
var resources = path.join(process.cwd(), "Tests", "AppKit", "Resources"),
|
||||
cibFolder = path.join(resources, "DropInCibs"),
|
||||
archives = {},
|
||||
cibs = {},
|
||||
failures = [];
|
||||
|
||||
fs.readdirSync(cibFolder).sort().forEach(function(aName)
|
||||
{
|
||||
cibs[aName] = DropInCibTree(aName, fs.readFileSync(path.join(cibFolder, aName), {encoding: 'utf-8'}));
|
||||
print(aName + ": " + cibs[aName].length + " top level objects");
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.join(resources, "DropInCibs.json"), JSON.stringify(cibs, null, 1) + "\n");
|
||||
|
||||
DropInSortedClasses().forEach(function(aClass)
|
||||
{
|
||||
var name = aClass.name;
|
||||
|
||||
if (!DropInIsPublicName(name) || !DropInClassIsView(aClass))
|
||||
return;
|
||||
|
||||
if (DropInArchiveExclusion(name))
|
||||
return print("excluded " + name + ": " + DropInArchiveExclusion(name));
|
||||
|
||||
try
|
||||
{
|
||||
var result = DropInArchiveRoundTrip(aClass),
|
||||
differences = DropInTreeDifferences(result.original, result.decoded, name, []);
|
||||
|
||||
if (differences.length)
|
||||
failures = failures.concat(differences);
|
||||
else
|
||||
archives[name] = { @"archive": result.archive, @"state": result.original };
|
||||
}
|
||||
catch (anException)
|
||||
{
|
||||
failures.push(name + ": " + String(anException));
|
||||
}
|
||||
});
|
||||
|
||||
if (failures.length)
|
||||
{
|
||||
failures.forEach(function(aLine) { print("failed " + aLine); });
|
||||
print(failures.length + " failures, no archive data written");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(resources, "DropInArchives.json"), JSON.stringify(archives, null, 1) + "\n");
|
||||
|
||||
print(Object.keys(archives).length + " archived view classes");
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* DropInArchiveTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "DropInArchiveFixtures.j"
|
||||
|
||||
/*
|
||||
Every application with an interface built in Xcode depends on cibs, and those
|
||||
cibs were written before the application's frameworks were last updated. An
|
||||
application that updates its frameworks does not rebuild them first, so the
|
||||
frameworks must keep reading what was already written. A new key may be added to
|
||||
an archive. Its absence from an older archive must then mean the old behavior,
|
||||
and not a new default.
|
||||
|
||||
The reference for a view is the view before it was archived, so a build cannot
|
||||
pass by restoring something else. A warning during decoding is a failure. The
|
||||
classes under test are those in the drop-in baseline, so a class cannot leave
|
||||
these tests unnoticed.
|
||||
|
||||
DropInArchiveGenerator.j saves the data these tests read. Each test reports every
|
||||
difference it finds.
|
||||
*/
|
||||
var fs = require("fs"),
|
||||
path = require("path");
|
||||
|
||||
@implementation DropInArchiveTest : OJTestCase
|
||||
{
|
||||
CPString resources;
|
||||
CPArray warnings;
|
||||
Function logWarning;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
resources = path.join(path.dirname(__filename), "Resources");
|
||||
warnings = [];
|
||||
logWarning = CPLog.warn;
|
||||
|
||||
CPLog.warn = function(aMessage)
|
||||
{
|
||||
warnings.push(String(aMessage));
|
||||
};
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
CPLog.warn = logWarning;
|
||||
}
|
||||
|
||||
- (id)jsonNamed:(CPString)aName
|
||||
{
|
||||
return JSON.parse(fs.readFileSync(path.join(resources, aName), {encoding: 'utf-8'}));
|
||||
}
|
||||
|
||||
- (CPArray)viewClassNames
|
||||
{
|
||||
return Object.keys([self jsonNamed:@"DropInBaseline.json"].construction).filter(function(aClassName)
|
||||
{
|
||||
return !DropInArchiveExclusion(aClassName);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)assertNoDifferences:(CPArray)differences
|
||||
{
|
||||
[self assertTrue:differences.length === 0 message:"\n" + differences.join("\n")];
|
||||
}
|
||||
|
||||
- (void)moveWarningsNamed:(CPString)aName into:(CPArray)differences
|
||||
{
|
||||
warnings.forEach(function(aWarning)
|
||||
{
|
||||
differences.push(aName + " warned: " + aWarning);
|
||||
});
|
||||
|
||||
warnings = [];
|
||||
}
|
||||
|
||||
/*
|
||||
A view that is archived and read back must be the same view, with the same
|
||||
content.
|
||||
*/
|
||||
- (void)testNewArchivesRestoreTheOriginalView
|
||||
{
|
||||
var differences = [];
|
||||
|
||||
[self viewClassNames].forEach(function(aClassName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = DropInArchiveRoundTrip(objj_getClass(aClassName));
|
||||
|
||||
DropInTreeDifferences(result.original, result.decoded, aClassName, differences);
|
||||
}
|
||||
catch (anException)
|
||||
{
|
||||
differences.push(aClassName + ": " + String(anException));
|
||||
}
|
||||
|
||||
[self moveWarningsNamed:aClassName into:differences];
|
||||
});
|
||||
|
||||
[self assertNoDifferences:differences];
|
||||
}
|
||||
|
||||
/*
|
||||
An archive written by main must restore the view it was written from.
|
||||
|
||||
The descriptions compared must be the same on every machine. A date, and the
|
||||
string value of a control that holds one, are recorded by the date's instant in
|
||||
UTC, because their text is in the local time zone (DropInSnapshot.j). GitHub's
|
||||
continuous integration runs this test in UTC, so run it in UTC as well as in the
|
||||
local time zone before committing a change:
|
||||
|
||||
TZ=UTC npx jake test
|
||||
*/
|
||||
- (void)testExistingArchivesRestoreTheOriginalView
|
||||
{
|
||||
var archivePath = path.join(resources, @"DropInArchives.json");
|
||||
|
||||
[self assertTrue:fs.existsSync(archivePath) message:@"DropInArchives.json has not been generated"];
|
||||
|
||||
var archives = [self jsonNamed:@"DropInArchives.json"],
|
||||
differences = [];
|
||||
|
||||
[self viewClassNames].forEach(function(aClassName)
|
||||
{
|
||||
var saved = archives[aClassName];
|
||||
|
||||
if (!saved)
|
||||
return differences.push(aClassName + ": no saved archive");
|
||||
|
||||
try
|
||||
{
|
||||
DropInTreeDifferences(saved.state, DropInSavedArchiveTree(saved.archive), aClassName, differences);
|
||||
}
|
||||
catch (anException)
|
||||
{
|
||||
differences.push(aClassName + ": " + String(anException));
|
||||
}
|
||||
|
||||
[self moveWarningsNamed:aClassName into:differences];
|
||||
});
|
||||
|
||||
[self assertNoDifferences:differences];
|
||||
}
|
||||
|
||||
/*
|
||||
These are fixed copies of real cibs, and they do not follow their sources. They are
|
||||
Tests/Manual/SmartFoldersTest/Resources/PredicateEditor.cib and
|
||||
Tests/Manual/TextEditor/Resources/DocumentWindow.cib, as of commit 5db5adac0.
|
||||
Loading one must produce the same windows, views and frames as main. The MainMenu
|
||||
cibs of those applications are not here. TextEditor's shows a menu bar, which needs
|
||||
a platform window, and SmartFoldersTest's connects a data source that is defined by
|
||||
the application.
|
||||
*/
|
||||
- (void)testExistingCibsProduceTheSameObjects
|
||||
{
|
||||
var expected = [self jsonNamed:@"DropInCibs.json"],
|
||||
differences = [];
|
||||
|
||||
for (var cibName in expected)
|
||||
{
|
||||
var rawString = fs.readFileSync(path.join(resources, "DropInCibs", cibName), {encoding: 'utf-8'}),
|
||||
current = DropInCibTree(cibName, rawString),
|
||||
expectedTrees = expected[cibName];
|
||||
|
||||
if (current.length !== expectedTrees.length)
|
||||
{
|
||||
differences.push(cibName + ": " + expectedTrees.length + " top level objects are now " + current.length);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var index = 0; index < current.length; index++)
|
||||
DropInTreeDifferences(JSON.parse(expectedTrees[index]), JSON.parse(current[index]), cibName, differences);
|
||||
|
||||
[self moveWarningsNamed:cibName into:differences];
|
||||
}
|
||||
|
||||
[self assertNoDifferences:differences];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* DropInBaselineGenerator.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "DropInSnapshot.j"
|
||||
|
||||
/*
|
||||
Saves the drop-in baseline. Run this only against a build of main, and only
|
||||
when a change to the drop-in contract is deliberate:
|
||||
|
||||
objj Tests/AppKit/DropInBaselineGenerator.j
|
||||
*/
|
||||
var fs = require("fs"),
|
||||
path = require("path");
|
||||
|
||||
function main(args, namedArgs)
|
||||
{
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
var skipped = [],
|
||||
snapshot = DropInCreateSnapshot(YES, skipped),
|
||||
baselinePath = path.join(process.cwd(), "Tests", "AppKit", "Resources", "DropInBaseline.json");
|
||||
|
||||
fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 1) + "\n");
|
||||
|
||||
print("Wrote " + baselinePath);
|
||||
print(snapshot.publicClasses.length + " public classes, " +
|
||||
Object.keys(snapshot.themeAttributes).length + " themed classes, " +
|
||||
Object.keys(snapshot.construction).length + " constructed views, " +
|
||||
Object.keys(snapshot.windows).length + " window styles");
|
||||
skipped.forEach(function(aLine) { print("skipped " + aLine); });
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* DropInBaselineTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "DropInSnapshot.j"
|
||||
|
||||
/*
|
||||
Compares the running frameworks with the recorded drop-in baseline. See
|
||||
DropInSnapshot.j for what the baseline holds and DropInBaselineGenerator.j for
|
||||
how to update it.
|
||||
|
||||
Each test reports every difference it finds. A list is more useful here than
|
||||
a failure at the first difference, because one change often affects several
|
||||
classes.
|
||||
*/
|
||||
var fs = require("fs"),
|
||||
path = require("path"),
|
||||
DropInBaseline = nil,
|
||||
DropInCurrent = nil;
|
||||
|
||||
@implementation DropInBaselineTest : OJTestCase
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
if (DropInBaseline)
|
||||
return;
|
||||
|
||||
var baselinePath = path.join(path.dirname(__filename), "Resources", "DropInBaseline.json");
|
||||
|
||||
DropInBaseline = JSON.parse(fs.readFileSync(baselinePath, {encoding: 'utf-8'}));
|
||||
DropInCurrent = DropInCreateSnapshot();
|
||||
}
|
||||
|
||||
- (void)assertNoDifferences:(CPArray)differences
|
||||
{
|
||||
[self assertTrue:differences.length === 0 message:"\n" + differences.join("\n")];
|
||||
}
|
||||
|
||||
/*
|
||||
A themed application reads these values whenever its theme supplies none, so a
|
||||
changed default changes the application.
|
||||
*/
|
||||
- (void)testThemeAttributeDefaultsAreUnchanged
|
||||
{
|
||||
var differences = [];
|
||||
|
||||
for (var className in DropInBaseline.themeAttributes)
|
||||
{
|
||||
var expected = DropInBaseline.themeAttributes[className],
|
||||
current = DropInCurrent.themeAttributes[className];
|
||||
|
||||
if (!current)
|
||||
{
|
||||
differences.push(className + ": class no longer declares theme attributes");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var attribute in expected)
|
||||
if (expected[attribute] !== current[attribute])
|
||||
differences.push(className + " " + attribute + ": " + expected[attribute] + " is now " + current[attribute]);
|
||||
}
|
||||
|
||||
[self assertNoDifferences:differences];
|
||||
}
|
||||
|
||||
- (void)testPublicClassesExist
|
||||
{
|
||||
var differences = DropInBaseline.publicClasses.filter(function(aName)
|
||||
{
|
||||
return !objj_getClass(aName);
|
||||
});
|
||||
|
||||
[self assertNoDifferences:differences];
|
||||
}
|
||||
|
||||
/*
|
||||
A public method may move to a superclass, since the class still responds to it.
|
||||
It may not disappear. This test cannot detect a method that remains but no
|
||||
longer does its work. Behavior tests cover that.
|
||||
*/
|
||||
- (void)testPublicMethodsExist
|
||||
{
|
||||
var differences = [];
|
||||
|
||||
DropInBaseline.publicClasses.forEach(function(aName)
|
||||
{
|
||||
var aClass = objj_getClass(aName);
|
||||
|
||||
if (!aClass)
|
||||
return;
|
||||
|
||||
DropInBaseline.instanceMethods[aName].forEach(function(aSelector)
|
||||
{
|
||||
if (!class_getInstanceMethod(aClass, aSelector))
|
||||
differences.push("-[" + aName + " " + aSelector + "]");
|
||||
});
|
||||
|
||||
DropInBaseline.classMethods[aName].forEach(function(aSelector)
|
||||
{
|
||||
if (!class_getClassMethod(aClass, aSelector))
|
||||
differences.push("+[" + aName + " " + aSelector + "]");
|
||||
});
|
||||
});
|
||||
|
||||
[self assertNoDifferences:differences];
|
||||
}
|
||||
|
||||
/*
|
||||
A view created in code has these values until the application changes them.
|
||||
*/
|
||||
- (void)testDefaultViewConstructionIsUnchanged
|
||||
{
|
||||
var differences = [];
|
||||
|
||||
for (var className in DropInBaseline.construction)
|
||||
{
|
||||
var expected = DropInBaseline.construction[className],
|
||||
current = DropInCurrent.construction[className];
|
||||
|
||||
if (!current)
|
||||
{
|
||||
differences.push(className + ": cannot be created");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var key in expected)
|
||||
{
|
||||
var expectedValue = String(expected[key]),
|
||||
currentValue = String(current[key]);
|
||||
|
||||
if (expectedValue !== currentValue)
|
||||
differences.push(className + " " + key + ": " + expectedValue + " is now " + currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
[self assertNoDifferences:differences];
|
||||
}
|
||||
|
||||
/*
|
||||
The public subviews of a window view are the controls a person sees in the
|
||||
title bar, so they are part of how an existing application looks.
|
||||
*/
|
||||
- (void)testWindowControlsAreUnchanged
|
||||
{
|
||||
var differences = [];
|
||||
|
||||
for (var style in DropInBaseline.windows)
|
||||
{
|
||||
var expected = String(DropInBaseline.windows[style]),
|
||||
current = String(DropInCurrent.windows[style]);
|
||||
|
||||
if (expected !== current)
|
||||
differences.push(style + ": " + expected + " is now " + current);
|
||||
}
|
||||
|
||||
[self assertNoDifferences:differences];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* DropInCompatibilityTest.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
Defaults that existing applications rely on. An application that updates its
|
||||
frameworks must keep these without any change of its own.
|
||||
*/
|
||||
@implementation DropInCompatibilityTest : OJTestCase
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// This will init the global var CPApp which are used internally in the AppKit
|
||||
[[CPApplication alloc] init];
|
||||
}
|
||||
|
||||
- (void)testDefaultThemeNameIsAristo2WithoutInfoPlistKey
|
||||
{
|
||||
[self assertNull:[[CPBundle mainBundle] objectForInfoDictionaryKey:@"CPDefaultTheme"]];
|
||||
[self assert:@"Aristo2" equals:[CPApplication defaultThemeName]];
|
||||
}
|
||||
|
||||
- (void)testResizableWindowHasOnlyACloseButton
|
||||
{
|
||||
var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0)
|
||||
styleMask:CPClosableWindowMask | CPResizableWindowMask],
|
||||
subviews = [[aWindow _windowView] subviews],
|
||||
count = [subviews count],
|
||||
buttonCount = 0;
|
||||
|
||||
while (count--)
|
||||
if ([subviews[count] isKindOfClass:[CPButton class]])
|
||||
buttonCount++;
|
||||
|
||||
[self assert:1 equals:buttonCount];
|
||||
}
|
||||
|
||||
- (void)testPopUpButtonKeepsTheDefaultButtonBezelStyle
|
||||
{
|
||||
var frame = CGRectMake(0.0, 0.0, 100.0, 24.0),
|
||||
popUpButton = [[CPPopUpButton alloc] initWithFrame:frame pullsDown:NO],
|
||||
button = [[CPButton alloc] initWithFrame:frame];
|
||||
|
||||
[self assert:[button bezelStyle] equals:[popUpButton bezelStyle]];
|
||||
}
|
||||
|
||||
- (void)testTableViewGridColorHasAClassDefault
|
||||
{
|
||||
[self assertFalse:[[[CPTableView themeAttributes] objectForKey:@"grid-color"] isEqual:[CPNull null]]];
|
||||
}
|
||||
|
||||
- (void)testTableViewAlternatingRowColorsHaveAClassDefault
|
||||
{
|
||||
[self assertFalse:[[[CPTableView themeAttributes] objectForKey:@"alternating-row-colors"] isEqual:[CPNull null]]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,521 @@
|
||||
/*
|
||||
* DropInSnapshot.j
|
||||
* AppKit
|
||||
*
|
||||
* Created by David Richardson on 31/10/2026.
|
||||
* Copyright David Richardson 2026.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
/*
|
||||
An application that updates its frameworks relies on more than documented
|
||||
behavior. It relies on the defaults a class supplies when no theme overrides
|
||||
them, on the public methods it calls and overrides, and on the state a control
|
||||
has when it is created. Together these form the drop-in contract.
|
||||
|
||||
This file records that contract as plain data. DropInBaselineGenerator.j saves
|
||||
a snapshot as the baseline, and DropInBaselineTest.j compares the running
|
||||
frameworks with it. Additions are allowed. Changes and removals are not.
|
||||
*/
|
||||
|
||||
var DropInConstructionGetters = [
|
||||
@"acceptsFirstResponder",
|
||||
@"alignment",
|
||||
@"autoresizingMask",
|
||||
@"backgroundColor",
|
||||
@"bezelStyle",
|
||||
@"controlSize",
|
||||
@"drawsBackground",
|
||||
@"isBordered",
|
||||
@"isEditable",
|
||||
@"isEnabled",
|
||||
@"isHidden",
|
||||
@"isSelectable",
|
||||
@"lineBreakMode",
|
||||
@"verticalAlignment"
|
||||
],
|
||||
|
||||
DropInWindowStyles = {
|
||||
@"borderless": CPBorderlessWindowMask,
|
||||
@"titled": CPTitledWindowMask,
|
||||
@"closable": CPClosableWindowMask,
|
||||
@"resizable": CPClosableWindowMask | CPResizableWindowMask,
|
||||
@"miniaturizable": CPClosableWindowMask | CPMiniaturizableWindowMask,
|
||||
@"full": CPClosableWindowMask | CPMiniaturizableWindowMask | CPResizableWindowMask,
|
||||
@"hud": CPHUDBackgroundWindowMask,
|
||||
@"hudClosable": CPHUDBackgroundWindowMask | CPClosableWindowMask
|
||||
};
|
||||
|
||||
/*
|
||||
The getters that report a BOOL, and the getters that report a view's content.
|
||||
*/
|
||||
var DropInBooleanGetters = [
|
||||
@"acceptsFirstResponder",
|
||||
@"drawsBackground",
|
||||
@"isBordered",
|
||||
@"isEditable",
|
||||
@"isEnabled",
|
||||
@"isHidden",
|
||||
@"isSelectable"
|
||||
],
|
||||
|
||||
DropInContentGetters = [
|
||||
@"color",
|
||||
@"doubleValue",
|
||||
@"indexOfSelectedItem",
|
||||
@"maxValue",
|
||||
@"minValue",
|
||||
@"numberOfColumns",
|
||||
@"numberOfItems",
|
||||
@"numberOfTabViewItems",
|
||||
@"objectValue",
|
||||
@"segmentCount",
|
||||
@"state",
|
||||
@"string",
|
||||
@"stringValue",
|
||||
@"tag",
|
||||
@"title"
|
||||
];
|
||||
|
||||
function DropInIsPublicName(aName)
|
||||
{
|
||||
return aName.charAt(0) !== "_" && aName.charAt(0) !== "$";
|
||||
}
|
||||
|
||||
function DropInSerializeObject(val, seen)
|
||||
{
|
||||
if (val === null || val === undefined)
|
||||
return nil;
|
||||
|
||||
if (typeof val === "function")
|
||||
return "[Function]";
|
||||
|
||||
if (typeof val !== "object")
|
||||
return val;
|
||||
|
||||
if (seen.indexOf(val) !== -1)
|
||||
return "[Circular]";
|
||||
|
||||
seen.push(val);
|
||||
|
||||
var result;
|
||||
|
||||
if (Array.isArray(val))
|
||||
{
|
||||
result = [];
|
||||
for (var i = 0; i < val.length; i++)
|
||||
{
|
||||
result[i] = DropInSerializeObject(val[i], seen);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = {};
|
||||
var keys = Object.keys(val).sort();
|
||||
for (var i = 0; i < keys.length; i++)
|
||||
{
|
||||
var k = keys[i];
|
||||
result[k] = DropInSerializeObject(val[k], seen);
|
||||
}
|
||||
}
|
||||
|
||||
seen.pop();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function DropInSafeJSONStringify(aValue)
|
||||
{
|
||||
return JSON.stringify(DropInSerializeObject(aValue, []));
|
||||
}
|
||||
|
||||
/*
|
||||
Reduces a value to a string that does not depend on object identity, so that
|
||||
two runs of the same framework produce the same snapshot.
|
||||
*/
|
||||
function DropInDescribeValue(aValue)
|
||||
{
|
||||
if (aValue === nil || aValue === undefined)
|
||||
return "nil";
|
||||
|
||||
/*
|
||||
Booleans, numbers and strings answer to isa as well, so they must be handled
|
||||
first, or they are recorded by class and their values are never compared.
|
||||
*/
|
||||
if (typeof aValue === "boolean" || typeof aValue === "number" || typeof aValue === "string")
|
||||
return String(aValue);
|
||||
|
||||
if (aValue === [CPNull null])
|
||||
return "CPNull";
|
||||
|
||||
if (aValue.isa)
|
||||
{
|
||||
/*
|
||||
A date is recorded by its instant in UTC. Its description is in the time zone
|
||||
of the machine that runs the test, so it differs between machines.
|
||||
*/
|
||||
if ([aValue isKindOfClass:[CPDate class]])
|
||||
return "CPDate:" + (isNaN(aValue.getTime()) ? "invalid" : aValue.toISOString());
|
||||
|
||||
if ([aValue isKindOfClass:[CPColor class]])
|
||||
return "CPColor:" + [aValue cssString];
|
||||
|
||||
/*
|
||||
The system font is a global setting that applications and tests change,
|
||||
so a default that uses it records only that fact.
|
||||
*/
|
||||
if ([aValue isKindOfClass:[CPFont class]])
|
||||
return [aValue isSystem] ? "CPFont:system" + ([aValue isBold] ? " bold" : "") : "CPFont:" + [aValue cssString];
|
||||
|
||||
if ([aValue isKindOfClass:[CPArray class]])
|
||||
return "[" + aValue.map(DropInDescribeValue).join(", ") + "]";
|
||||
|
||||
return aValue.isa.name;
|
||||
}
|
||||
|
||||
return DropInSafeJSONStringify(aValue);
|
||||
}
|
||||
|
||||
function DropInMethodNames(aClass)
|
||||
{
|
||||
return class_copyMethodList(aClass).map(function(aMethod)
|
||||
{
|
||||
return method_getName(aMethod);
|
||||
}).filter(DropInIsPublicName).sort();
|
||||
}
|
||||
|
||||
function DropInThemeAttributes(aClass)
|
||||
{
|
||||
if (class_copyMethodList(aClass.isa).map(method_getName).indexOf("themeAttributes") === -1)
|
||||
return nil;
|
||||
|
||||
var attributes = [aClass themeAttributes];
|
||||
|
||||
if (!attributes)
|
||||
return nil;
|
||||
|
||||
var result = {},
|
||||
keys = [[attributes allKeys] sortedArrayUsingSelector:@selector(compare:)];
|
||||
|
||||
keys.forEach(function(aKey)
|
||||
{
|
||||
result[aKey] = DropInDescribeValue([attributes objectForKey:aKey]);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function DropInSubviewClasses(aView)
|
||||
{
|
||||
return [aView subviews].map(function(aSubview)
|
||||
{
|
||||
return aSubview.isa.name;
|
||||
}).filter(DropInIsPublicName).sort();
|
||||
}
|
||||
|
||||
function DropInViewState(aView, throwIfError)
|
||||
{
|
||||
var result = {
|
||||
@"frameSize": DropInDescribeValue([aView frameSize]),
|
||||
@"subviews": DropInSubviewClasses(aView)
|
||||
};
|
||||
|
||||
DropInConstructionGetters.forEach(function(aGetter)
|
||||
{
|
||||
var selector = CPSelectorFromString(aGetter);
|
||||
|
||||
if ([aView respondsToSelector:selector])
|
||||
{
|
||||
try
|
||||
{
|
||||
result[aGetter] = DropInDescribeValue([aView performSelector:selector]);
|
||||
}
|
||||
catch (anException)
|
||||
{
|
||||
if (throwIfError)
|
||||
throw anException;
|
||||
|
||||
result[aGetter] = "[Exception: " + String(anException) + "]";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function DropInDefaultConstruction(aClass, throwIfError)
|
||||
{
|
||||
return DropInViewState([[aClass alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)], throwIfError);
|
||||
}
|
||||
|
||||
/*
|
||||
Lists the public views below a view, in order. A private view is the class's own
|
||||
concern and is left out, but the public views inside it are the view's content.
|
||||
An observed view has a generated isa, so its name comes from its class method.
|
||||
*/
|
||||
function DropInPublicSubviews(aView)
|
||||
{
|
||||
return [aView subviews].reduce(function(aList, aSubview)
|
||||
{
|
||||
return aList.concat(DropInIsPublicName([aSubview className]) ? [aSubview] : DropInPublicSubviews(aSubview));
|
||||
}, []);
|
||||
}
|
||||
|
||||
/*
|
||||
Describes the state of one view. An unset BOOL is nil and means NO, so a BOOL is
|
||||
recorded by its truth value.
|
||||
*/
|
||||
function DropInViewTreeState(aView)
|
||||
{
|
||||
var state = {};
|
||||
|
||||
DropInConstructionGetters.concat(DropInContentGetters).forEach(function(aGetter)
|
||||
{
|
||||
var selector = CPSelectorFromString(aGetter);
|
||||
|
||||
if (![aView respondsToSelector:selector])
|
||||
return;
|
||||
|
||||
var value = [aView performSelector:selector];
|
||||
|
||||
/*
|
||||
A control that holds a date gives its string value in the time zone of the
|
||||
machine that runs the test, because the browser formats it, not Cappuccino. It
|
||||
is recorded by the date's instant instead, so that every machine records the
|
||||
same snapshot.
|
||||
*/
|
||||
if (aGetter === @"stringValue" && [aView respondsToSelector:@selector(objectValue)] && [[aView objectValue] isKindOfClass:[CPDate class]])
|
||||
value = [aView objectValue];
|
||||
|
||||
state[aGetter] = DropInDescribeValue(DropInBooleanGetters.indexOf(aGetter) !== -1 ? !!value : value);
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/*
|
||||
Describes a view and everything public below it.
|
||||
*/
|
||||
function DropInViewTree(aView)
|
||||
{
|
||||
return {
|
||||
@"class": [aView className],
|
||||
@"frame": DropInDescribeValue([aView frame]),
|
||||
@"state": DropInViewTreeState(aView),
|
||||
@"subviews": DropInPublicSubviews(aView).map(function(aSubview)
|
||||
{
|
||||
return DropInViewTree(aSubview);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function DropInMenuTree(aMenu)
|
||||
{
|
||||
return [aMenu itemArray].map(function(anItem)
|
||||
{
|
||||
return {
|
||||
@"title": [anItem title],
|
||||
@"keyEquivalent": [anItem keyEquivalent],
|
||||
@"submenu": [anItem hasSubmenu] ? DropInMenuTree([anItem submenu]) : nil
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Describes what a cib produced. Objects other than windows, views and menus are
|
||||
recorded by class alone.
|
||||
*/
|
||||
function DropInObjectTree(anObject)
|
||||
{
|
||||
if ([anObject isKindOfClass:[CPWindow class]])
|
||||
return { @"class": anObject.isa.name, @"title": [anObject title], @"contentView": DropInViewTree([anObject contentView]) };
|
||||
|
||||
if ([anObject isKindOfClass:[CPView class]])
|
||||
return DropInViewTree(anObject);
|
||||
|
||||
if ([anObject isKindOfClass:[CPMenu class]])
|
||||
return { @"class": anObject.isa.name, @"items": DropInMenuTree(anObject) };
|
||||
|
||||
return { @"class": anObject.isa.name };
|
||||
}
|
||||
|
||||
/*
|
||||
The owner of the reference cibs. It has the outlets and actions that those cibs
|
||||
connect to it, so that loading them gives no warnings.
|
||||
*/
|
||||
@implementation _DropInCibOwner : CPObject
|
||||
{
|
||||
id folderNameField;
|
||||
id predicateEditor;
|
||||
id predicateSheet;
|
||||
id textView;
|
||||
id window;
|
||||
}
|
||||
|
||||
- (void)closeSheet:(id)aSender
|
||||
{
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
A scroll view takes its scroller style from a global setting, and that style changes
|
||||
the frames and views inside it. Applications and tests change the setting, so a
|
||||
description that is saved or compared is always made with the same style.
|
||||
*/
|
||||
function DropInWithFixedScrollerStyle(aFunction)
|
||||
{
|
||||
var scrollerStyle = [CPScrollView globalScrollerStyle];
|
||||
|
||||
[CPScrollView setGlobalScrollerStyle:CPScrollerStyleLegacy];
|
||||
|
||||
try
|
||||
{
|
||||
return aFunction();
|
||||
}
|
||||
finally
|
||||
{
|
||||
[CPScrollView setGlobalScrollerStyle:scrollerStyle];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Loads a cib and describes its top level objects, sorted so that the order in
|
||||
which the cib lists them does not matter. Layout changes frames and adds subviews,
|
||||
so it is complete before anything is described.
|
||||
*/
|
||||
function DropInCibTree(aCibName, aRawString)
|
||||
{
|
||||
var topLevelObjects = [],
|
||||
cib = [[CPCib alloc] _initWithData:[CPData dataWithRawString:aRawString] bundle:nil cibName:aCibName];
|
||||
|
||||
return DropInWithFixedScrollerStyle(function()
|
||||
{
|
||||
[cib instantiateCibWithExternalNameTable:@{ CPCibOwner: [[_DropInCibOwner alloc] init], CPCibTopLevelObjects: topLevelObjects }];
|
||||
[_CPDisplayServer run];
|
||||
|
||||
return topLevelObjects.map(DropInObjectTree).map(function(aTree)
|
||||
{
|
||||
return JSON.stringify(aTree);
|
||||
}).sort();
|
||||
});
|
||||
}
|
||||
|
||||
function DropInClassIsView(aClass)
|
||||
{
|
||||
var viewClass = [CPView class];
|
||||
|
||||
for (; aClass; aClass = class_getSuperclass(aClass))
|
||||
if (aClass === viewClass)
|
||||
return YES;
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
function DropInSortedClasses()
|
||||
{
|
||||
var classes = [],
|
||||
count = objj_getClassList(classes, 1 << 16);
|
||||
|
||||
return classes.slice(0, count).filter(function(aClass)
|
||||
{
|
||||
return aClass && aClass.name.indexOf("$") !== 0;
|
||||
}).sort(function(lhs, rhs)
|
||||
{
|
||||
return lhs.name < rhs.name ? -1 : (lhs.name > rhs.name ? 1 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Some classes cannot be created without a surrounding object, or without a DOM.
|
||||
A class that cannot be created is left out of the construction section, and the
|
||||
comparison does not report it. This reduces coverage, but it cannot report a
|
||||
false difference.
|
||||
|
||||
A generator passes throwIfError, so that a getter that throws is not saved as
|
||||
expected state, and passes skipped, to list each class or window style that is
|
||||
left out.
|
||||
*/
|
||||
function DropInCreateSnapshot(throwIfError, skipped)
|
||||
{
|
||||
var snapshot = {
|
||||
@"publicClasses": [],
|
||||
@"themeAttributes": {},
|
||||
@"instanceMethods": {},
|
||||
@"classMethods": {},
|
||||
@"construction": {},
|
||||
@"windows": {}
|
||||
};
|
||||
|
||||
DropInSortedClasses().forEach(function(aClass)
|
||||
{
|
||||
var name = aClass.name,
|
||||
isPublic = DropInIsPublicName(name);
|
||||
|
||||
try
|
||||
{
|
||||
var attributes = DropInThemeAttributes(aClass);
|
||||
|
||||
if (attributes)
|
||||
snapshot.themeAttributes[name] = attributes;
|
||||
}
|
||||
catch (anException)
|
||||
{
|
||||
if (skipped)
|
||||
skipped.push(name + " theme attributes: " + String(anException).slice(0, 80));
|
||||
}
|
||||
|
||||
if (!isPublic)
|
||||
return;
|
||||
|
||||
snapshot.publicClasses.push(name);
|
||||
snapshot.instanceMethods[name] = DropInMethodNames(aClass);
|
||||
snapshot.classMethods[name] = DropInMethodNames(aClass.isa);
|
||||
|
||||
if (!DropInClassIsView(aClass))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
snapshot.construction[name] = DropInDefaultConstruction(aClass, throwIfError);
|
||||
}
|
||||
catch (anException)
|
||||
{
|
||||
if (skipped)
|
||||
skipped.push(name + " construction: " + String(anException).slice(0, 80));
|
||||
}
|
||||
});
|
||||
|
||||
for (var style in DropInWindowStyles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0)
|
||||
styleMask:DropInWindowStyles[style]];
|
||||
|
||||
snapshot.windows[style] = DropInSubviewClasses([aWindow _windowView]);
|
||||
}
|
||||
catch (anException)
|
||||
{
|
||||
if (skipped)
|
||||
skipped.push(style + " window style: " + String(anException).slice(0, 80));
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"SmartFoldersPredicateEditor.cib": [
|
||||
"{\"class\":\"CPApplication\"}",
|
||||
"{\"class\":\"CPWindow\",\"title\":\"Predicate Editor\",\"contentView\":{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":1,\\\"y\\\":1},\\\"size\\\":{\\\"height\\\":290,\\\"width\\\":551}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPTextField\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":63,\\\"y\\\":12},\\\"size\\\":{\\\"height\\\":29,\\\"width\\\":474}}\",\"state\":{\"acceptsFirstResponder\":\"true\",\"alignment\":\"4\",\"autoresizingMask\":\"34\",\"backgroundColor\":\"CPColor:rgba(255, 255, 255, 1)\",\"bezelStyle\":\"0\",\"controlSize\":\"0\",\"drawsBackground\":\"true\",\"isBordered\":\"false\",\"isEditable\":\"true\",\"isEnabled\":\"true\",\"isHidden\":\"false\",\"isSelectable\":\"true\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"\",\"stringValue\":\"\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":29,\\\"width\\\":474}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPTextField\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":19,\\\"y\\\":18},\\\"size\\\":{\\\"height\\\":17,\\\"width\\\":41}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"alignment\":\"4\",\"autoresizingMask\":\"36\",\"backgroundColor\":\"CPColor:rgba(175, 175, 175, 1)\",\"bezelStyle\":\"0\",\"controlSize\":\"0\",\"drawsBackground\":\"false\",\"isBordered\":\"false\",\"isEditable\":\"false\",\"isEnabled\":\"true\",\"isHidden\":\"false\",\"isSelectable\":\"false\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"Name:\",\"stringValue\":\"Name:\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":17,\\\"width\\\":41}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPScrollView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":20,\\\"y\\\":55},\\\"size\\\":{\\\"height\\\":184,\\\"width\\\":512}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPClipView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":1,\\\"y\\\":1},\\\"size\\\":{\\\"height\\\":182,\\\"width\\\":510}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"CPColor:rgba(232, 232, 232, 1)\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPPredicateEditor\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":182,\\\"width\\\":510}}\",\"state\":{\"acceptsFirstResponder\":\"true\",\"alignment\":\"0\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"controlSize\":\"0\",\"isEditable\":\"true\",\"isEnabled\":\"true\",\"isHidden\":\"false\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"nil\",\"doubleValue\":\"0\",\"objectValue\":\"nil\",\"stringValue\":\"\",\"tag\":\"0\"},\"subviews\":[]}]},{\"class\":\"CPScroller\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":-100,\\\"y\\\":269},\\\"size\\\":{\\\"height\\\":7,\\\"width\\\":360}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"alignment\":\"0\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"nil\",\"controlSize\":\"0\",\"isEnabled\":\"false\",\"isHidden\":\"true\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"0\",\"stringValue\":\"0\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":7,\\\"width\\\":360}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPScroller\",\"frame\":\"{\\\"origin\\\":{\\\"Line truncated
|
||||
],
|
||||
"TextEditorDocumentWindow.cib": [
|
||||
"{\"class\":\"CPApplication\"}",
|
||||
"{\"class\":\"CPWindow\",\"title\":\"Window\",\"contentView\":{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":1,\\\"y\\\":92},\\\"size\\\":{\\\"height\\\":168,\\\"width\\\":474}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPScrollView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":202,\\\"width\\\":476}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPClipView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":202,\\\"width\\\":469}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPTextView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":270,\\\"width\\\":469}}\",\"state\":{\"acceptsFirstResponder\":\"true\",\"autoresizingMask\":\"18\",\"backgroundColor\":\"CPColor:rgba(255, 255, 255, 1)\",\"isEditable\":\"true\",\"isHidden\":\"false\",\"isSelectable\":\"true\",\"objectValue\":\"CPTextStorage\",\"string\":\"\",\"stringValue\":\"\",\"tag\":\"0\"},\"subviews\":[]}]},{\"class\":\"CPScroller\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":-100,\\\"y\\\":355},\\\"size\\\":{\\\"height\\\":7,\\\"width\\\":240}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"alignment\":\"0\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"nil\",\"controlSize\":\"0\",\"isEnabled\":\"false\",\"isHidden\":\"true\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"0\",\"stringValue\":\"0\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":7,\\\"width\\\":240}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPScroller\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":469,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":202,\\\"width\\\":7}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"alignment\":\"0\",\"autoresizingMask\":\"8\",\"backgroundColor\":\"nil\",\"controlSize\":\"0\",\"isEnabled\":\"true\",\"isHidden\":\"false\",\"lineBreakMode\":\"2\",\"verticalAlignment\":\"1\",\"doubleValue\":\"0\",\"objectValue\":\"0\",\"stringValue\":\"0\",\"tag\":\"0\"},\"subviews\":[{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":202,\\\"width\\\":7}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":202},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":151.1259259259259,\\\"width\\\":7}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"nil\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]}]},{\"class\":\"CPView\",\"frame\":\"{\\\"origin\\\":{\\\"x\\\":0,\\\"y\\\":0},\\\"size\\\":{\\\"height\\\":0,\\\"width\\\":0}}\",\"state\":{\"acceptsFirstResponder\":\"false\",\"autoresizingMask\":\"0\",\"backgroundColor\":\"CPColor:rgba(255, 255, 255, 1)\",\"isHidden\":\"false\",\"tag\":\"-1\"},\"subviews\":[]},{\"class\":\"CPClipView\",\"frame\":\"{\\\"origin\\\":{\\\"xLine truncated
|
||||
]
|
||||
}
|
||||
@@ -0,0 +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;16E;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;18E;D;K;6;CP$UIDd;2;20E;D;K;6;CP$UIDd;2;21E;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;22E;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;33E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;39E;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;61E;D;K;6;CP$UIDd;2;62E;D;K;6;CP$UIDd;2;64E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;65E;D;K;6;CP$UIDd;1;0E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;33E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;53E;D;K;6;CP$UIDd;2;54E;D;K;6;CP$UIDd;2;53E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;64E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;65E;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;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;66E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;12E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;2;67E;E;D;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;22E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;11E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;68E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;28E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;69E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;35E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;70E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;24E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;71E;E;D;K;10;$classnameS;21;CPCibControlConnectorK;8;$classesA;S;21;CPCibControlConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;19E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;64E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;11E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;72E;E;D;K;6;$classD;K;6;CP$UIDd;2;19E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;65E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;11E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;72E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;73E;E;D;K;10;$classnameS;20;_CPCibWindowTemplateK;8;$classesA;S;20;_CPCibWindowTemplateS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;23E;K;30;_CPCibWindowTemplateMinSizeKeyD;K;6;CP$UIDd;2;74E;K;30;_CPCibWindowTemplateMaxSizeKeyD;K;6;CP$UIDd;2;75E;K;32;_CPCibWindowTemplateViewClassKeyD;K;6;CP$UIDd;1;0E;K;34;_CPCibWindowTemplateWindowClassKeyD;K;6;CP$UIDd;2;76E;K;30;_CPCiLine truncated
|
||||
@@ -0,0 +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;16E;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;17E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;27E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;32E;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;38E;D;K;6;CP$UIDd;2;40E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;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;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;41E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;12E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;2;42E;E;D;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;27E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;43E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;19E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;44E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;45E;E;D;K;10;$classnameS;20;_CPCibWindowTemplateK;8;$classesA;S;20;_CPCibWindowTemplateS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;18E;K;32;_CPCibWindowTemplateViewClassKeyD;K;6;CP$UIDd;2;32E;K;34;_CPCibWindowTemplateWindowClassKeyD;K;6;CP$UIDd;2;46E;K;30;_CPCibWindowTemplateWTFlagsKeyD;K;6;CP$UIDd;2;47E;K;33;_CPCibWindowTemplateWindowRectKeyD;K;6;CP$UIDd;2;48E;K;33;_CPCibWindowTemplateScreenRectKeyD;K;6;CP$UIDd;2;49E;K;30;_CPCibWindowTempatStyleMaskKeyD;K;6;CP$UIDd;2;50E;K;34;_CPCibWindowTemplateWindowTitleKeyD;K;6;CP$UIDd;2;51E;K;33;_CPCibWindowTemplateWindowViewKeyD;K;6;CP$UIDd;2;21E;E;D;K;10;$classnameS;6;CPViewK;8;$classesA;S;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;20E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;53E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;53E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;54E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;55E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;56E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;60E;E;D;K;10;$classnameS;12;CPScrollViewK;8;$classesA;S;12;CPScrollViewS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;22E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;21E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;53E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;53E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;61E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;21E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;55E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;62E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;2;64E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;65E;K;23;CPScrollViewContentViewD;K;6;CP$UIDd;2;25E;K;29;CPScrollViewHeaderClipViewKeyD;K;6;CP$UIDd;1;0E;K;21;CPScrollViewVScrollerD;K;6;CP$UIDd;2;30E;K;21;CPScrollViewHScrollerD;K;6Line truncated
|
||||
@@ -1,6 +1,8 @@
|
||||
@import <Foundation/CPMapTable.j>
|
||||
@import <Foundation/CPDictionary.j>
|
||||
@import <Foundation/CPEnumerator.j>
|
||||
@import <Foundation/CPKeyedArchiver.j>
|
||||
@import <Foundation/CPKeyedUnarchiver.j>
|
||||
@import <Foundation/CPMutableArray.j>
|
||||
@import <OJUnit/OJTestCase.j>
|
||||
|
||||
@@ -139,6 +141,18 @@
|
||||
[self assertTrue:[foundValues containsObject:@"value3"]];
|
||||
}
|
||||
|
||||
- (void)testArchivingPreservesEntries
|
||||
{
|
||||
[map_table setObject:@"first" forKey:@"one"];
|
||||
[map_table setObject:@"second" forKey:@"two"];
|
||||
|
||||
var decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:map_table]];
|
||||
|
||||
[self assert:2 equals:[decoded count]];
|
||||
[self assert:@"first" equals:[decoded objectForKey:@"one"]];
|
||||
[self assert:@"second" equals:[decoded objectForKey:@"two"]];
|
||||
}
|
||||
|
||||
- (void)testDictionaryRepresentation
|
||||
{
|
||||
[map_table setObject:@"value1" forKey:@"key1"];
|
||||
|
||||
@@ -434,6 +434,41 @@
|
||||
[self assertTrue:[predicate evaluateWithObject:dict] message:"Predicate " + predicate + " should evaluate to TRUE"];
|
||||
}
|
||||
|
||||
- (void)testCustomSelectorPredicateParsing
|
||||
{
|
||||
var predicate = [CPPredicate predicateWithFormat:@"SELF isKindOfClass: %@", [CPArray class]];
|
||||
|
||||
[self assert:CPCustomSelectorPredicateOperatorType equals:[predicate predicateOperatorType]];
|
||||
[self assert:@selector(isKindOfClass:) equals:[predicate customSelector]];
|
||||
[self assertTrue:[predicate evaluateWithObject:[CPArray array]]];
|
||||
[self assertFalse:[predicate evaluateWithObject:@{}]];
|
||||
|
||||
var negated = [CPPredicate predicateWithFormat:@"NOT SELF isKindOfClass: %@", [CPArray class]];
|
||||
|
||||
[self assertFalse:[negated evaluateWithObject:[CPArray array]]];
|
||||
[self assertTrue:[negated evaluateWithObject:@{}]];
|
||||
|
||||
// A parse failure is reported through CPLogConsole, which is captured here.
|
||||
var logConsole = CPLogConsole,
|
||||
messages = [];
|
||||
|
||||
CPLogConsole = function(aString)
|
||||
{
|
||||
messages.push(aString);
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
[self assertNull:[CPPredicate predicateWithFormat:@"ANY SELF isKindOfClass: %@", [CPArray class]]];
|
||||
}
|
||||
finally
|
||||
{
|
||||
CPLogConsole = logConsole;
|
||||
}
|
||||
|
||||
[self assertTrue:messages.length > 0];
|
||||
}
|
||||
|
||||
- (void)testExpressionAndPredicateIsEqual
|
||||
{
|
||||
var cexp1 = [CPExpression expressionForConstantValue:2],
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
CPLocale _locale;
|
||||
CPData _data;
|
||||
CPDate _date;
|
||||
CPDate _januaryDate;
|
||||
CPDate _julyDate;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
@@ -34,6 +36,42 @@
|
||||
_locale = [[CPLocale alloc] initWithLocaleIdentifier:@"en_US"];
|
||||
_data = [CPData dataWithRawString:@"Data with string"];
|
||||
_date = [[CPDate alloc] initWithString:@"2011-10-05 16:34:38 +0900"];
|
||||
|
||||
/*
|
||||
The rules of a zone decide its offset and abbreviation on a given date. Fixed
|
||||
dates in January and July give the same result on any day and on any host.
|
||||
A zone with daylight saving time differs between them, and a zone without it
|
||||
has the same offset on both.
|
||||
*/
|
||||
_januaryDate = [[CPDate alloc] initWithString:@"2026-01-15 12:00:00 +0000"];
|
||||
_julyDate = [[CPDate alloc] initWithString:@"2026-07-15 12:00:00 +0000"];
|
||||
}
|
||||
|
||||
- (void)assertPacificTimeZone:(CPTimeZone)timeZone
|
||||
{
|
||||
[self assert:@"America/Los_Angeles" equals:[timeZone name]];
|
||||
|
||||
[self assert:@"PST" equals:[timeZone abbreviationForDate:_januaryDate]];
|
||||
[self assert:(-480 * 60) equals:[timeZone secondsFromGMTForDate:_januaryDate]];
|
||||
[self assertFalse:[timeZone isDaylightSavingTimeForDate:_januaryDate]];
|
||||
|
||||
[self assert:@"PDT" equals:[timeZone abbreviationForDate:_julyDate]];
|
||||
[self assert:(-420 * 60) equals:[timeZone secondsFromGMTForDate:_julyDate]];
|
||||
[self assertTrue:[timeZone isDaylightSavingTimeForDate:_julyDate]];
|
||||
|
||||
// The abbreviation and the offset are both those of the current date.
|
||||
var now = [CPDate date];
|
||||
|
||||
[self assert:[timeZone abbreviationForDate:now] equals:[timeZone abbreviation]];
|
||||
[self assert:[timeZone secondsFromGMTForDate:now] equals:[timeZone secondsFromGMT]];
|
||||
[self assert:@"America/Los_Angeles (" + [timeZone abbreviation] + ") offset " + [timeZone secondsFromGMT] equals:[timeZone description]];
|
||||
|
||||
[self assert:@"Pacific Standard Time" equals:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale]];
|
||||
[self assert:@"PST" equals:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale]];
|
||||
[self assert:@"Pacific Daylight Time" equals:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale]];
|
||||
[self assert:@"PDT" equals:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale]];
|
||||
[self assert:@"Pacific Time" equals:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale]];
|
||||
[self assert:@"PT" equals:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale]];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
@@ -43,17 +81,8 @@
|
||||
|
||||
- (void)testTimeZoneWithAbbreviation
|
||||
{
|
||||
var timeZone = [CPTimeZone timeZoneWithAbbreviation:@"PDT"];
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PDT"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
|
||||
[self assertPacificTimeZone:[CPTimeZone timeZoneWithAbbreviation:@"PDT"]];
|
||||
[self assertPacificTimeZone:[CPTimeZone timeZoneWithAbbreviation:@"PST"]];
|
||||
}
|
||||
|
||||
- (void)testTimeZoneWithWrongAbbreviation
|
||||
@@ -64,26 +93,7 @@
|
||||
|
||||
- (void)testTimeZoneWithName
|
||||
{
|
||||
var timeZone = [CPTimeZone timeZoneWithName:@"America/Los_Angeles"];
|
||||
|
||||
// The current implementation can return daylight saving time or standard time depending on the current date
|
||||
if ([timeZone abbreviation] === @"PDT") {
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PDT"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
|
||||
} else {
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PST"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
|
||||
}
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
|
||||
[self assertPacificTimeZone:[CPTimeZone timeZoneWithName:@"America/Los_Angeles"]];
|
||||
}
|
||||
|
||||
- (void)testTimeZoneWithWrongName
|
||||
@@ -109,24 +119,8 @@
|
||||
{
|
||||
var timeZone = [CPTimeZone timeZoneWithName:@"America/Los_Angeles" data:_data];
|
||||
|
||||
if ([timeZone abbreviation] === @"PDT") {
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PDT"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
|
||||
} else {
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PST"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
|
||||
}
|
||||
[self assert:[timeZone data] equals:_data];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
|
||||
[self assertPacificTimeZone:timeZone];
|
||||
[self assert:_data equals:[timeZone data]];
|
||||
}
|
||||
|
||||
- (void)testTimeZoneWithWrongNameWithData
|
||||
@@ -179,25 +173,7 @@
|
||||
|
||||
- (void)testInitTimeZoneWithName
|
||||
{
|
||||
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"];
|
||||
|
||||
if ([timeZone abbreviation] === @"PDT") {
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PDT"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
|
||||
} else {
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PST"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
|
||||
}
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
|
||||
[self assertPacificTimeZone:[[CPTimeZone alloc] initWithName:@"America/Los_Angeles"]];
|
||||
}
|
||||
|
||||
- (void)testInitTimeZoneWithWrongName
|
||||
@@ -223,24 +199,8 @@
|
||||
{
|
||||
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles" data:_data];
|
||||
|
||||
if ([timeZone abbreviation] === @"PDT") {
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PDT"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
|
||||
} else {
|
||||
[self assert:[timeZone name] equals:@"America/Los_Angeles"];
|
||||
[self assert:[timeZone abbreviation] equals:@"PST"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
|
||||
}
|
||||
[self assert:[timeZone data] equals:_data];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Pacific Standard Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"PST"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Pacific Daylight Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"PDT"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Pacific Time"];
|
||||
[self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"PT"];
|
||||
[self assertPacificTimeZone:timeZone];
|
||||
[self assert:_data equals:[timeZone data]];
|
||||
}
|
||||
|
||||
- (void)testInitTimeZoneWithWrongNameWithData
|
||||
@@ -262,14 +222,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The local time zone depends on the host, and many zones have no curated
|
||||
abbreviation, so a named zone is used.
|
||||
*/
|
||||
- (void)testAbbreviationWithDate
|
||||
{
|
||||
var timeZone = [CPTimeZone localTimeZone],
|
||||
abbreviation = [timeZone abbreviationForDate:_date];
|
||||
var timeZone = [CPTimeZone timeZoneWithName:@"America/Los_Angeles"];
|
||||
|
||||
[self assert:(abbreviation !== nil) equals:YES];
|
||||
var knownAbbreviations = [CPTimeZone abbreviationDictionary];
|
||||
[self assert:[knownAbbreviations containsKey:abbreviation] equals:YES];
|
||||
[self assert:@"PDT" equals:[timeZone abbreviationForDate:_date]];
|
||||
[self assert:@"PST" equals:[timeZone abbreviationForDate:_januaryDate]];
|
||||
[self assertNotNull:[[CPTimeZone localTimeZone] abbreviationForDate:_date]];
|
||||
}
|
||||
|
||||
- (void)testAbbreviationWithNilDate
|
||||
@@ -301,22 +264,17 @@
|
||||
{
|
||||
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"];
|
||||
|
||||
if ([timeZone abbreviation] === @"PDT") {
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-420 * 60)];
|
||||
} else {
|
||||
[self assert:[timeZone secondsFromGMT] equals:(-480 * 60)];
|
||||
}
|
||||
[self assert:(-480 * 60) equals:[timeZone secondsFromGMTForDate:_januaryDate]];
|
||||
[self assert:(-420 * 60) equals:[timeZone secondsFromGMTForDate:_julyDate]];
|
||||
[self assert:[timeZone secondsFromGMTForDate:[CPDate date]] equals:[timeZone secondsFromGMT]];
|
||||
}
|
||||
|
||||
- (void)testDescription
|
||||
{
|
||||
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"];
|
||||
var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"],
|
||||
expected = [timeZone secondsFromGMT] === -480 * 60 ? @"America/Los_Angeles (PST) offset -28800" : @"America/Los_Angeles (PDT) offset -25200";
|
||||
|
||||
if ([timeZone abbreviation] === @"PDT") {
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"];
|
||||
} else {
|
||||
[self assert:[timeZone description] equals:@"America/Los_Angeles (PST) offset -28800"];
|
||||
}
|
||||
[self assert:expected equals:[timeZone description]];
|
||||
}
|
||||
|
||||
- (void)testLocalizedName
|
||||
@@ -401,11 +359,14 @@
|
||||
|
||||
- (void)testMSKCorrectCurrentValue
|
||||
{
|
||||
// MSD/MSK resolve to Europe/Moscow.
|
||||
// Russia abolished DST in 2014, so there is no distinct summer offset.
|
||||
// The current permanent offset is +03:00 (10800 seconds).
|
||||
// MSD and MSK resolve to Europe/Moscow, which has no daylight saving time.
|
||||
var timeZone = [CPTimeZone timeZoneWithAbbreviation:@"MSD"];
|
||||
[self assert:[timeZone secondsFromGMT] equals:(180 * 60)];
|
||||
|
||||
[self assert:@"Europe/Moscow" equals:[timeZone name]];
|
||||
[self assert:(180 * 60) equals:[timeZone secondsFromGMTForDate:_januaryDate]];
|
||||
[self assert:(180 * 60) equals:[timeZone secondsFromGMTForDate:_julyDate]];
|
||||
[self assertFalse:[timeZone isDaylightSavingTimeForDate:_julyDate]];
|
||||
[self assert:[timeZone secondsFromGMTForDate:[CPDate date]] equals:[timeZone secondsFromGMT]];
|
||||
}
|
||||
|
||||
- (void)testKnownTimeZoneNamesUsesIntlWhenAvailable
|
||||
|
||||
Reference in new issue
Block a user