mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-23 08:38:53 +00:00
Merge branch 'main' into refactor-remove-nativehost-target
# Conflicts: # AppKit/CPTextView/_CPTableTextAttachment.j
This commit is contained in:
commit
4edcf0fecc
16 files changed
+692
-161
No files matched your search
@@ -93,6 +93,7 @@
|
||||
@import "CPSlider.j"
|
||||
@import "CPSound.j"
|
||||
@import "CPSplitView.j"
|
||||
@import "CPStackView.j"
|
||||
@import "CPStepper.j"
|
||||
@import "CPTableColumn.j"
|
||||
@import "CPTableView.j"
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
@import "CPButton.j"
|
||||
@import "CPTableColumn.j"
|
||||
@import "CPTableView.j"
|
||||
@import "CPTreeNode.j"
|
||||
|
||||
@global CPApp
|
||||
|
||||
@@ -2531,6 +2532,8 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted)
|
||||
selector:@selector(outlineViewSelectionDidChange:)
|
||||
name:CPOutlineViewSelectionDidChangeNotification
|
||||
object:aSource];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (void)unbind:(CPString)aBinding forObject:(id)anObject
|
||||
|
||||
+135
-18
@@ -20,7 +20,80 @@
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
/*
|
||||
* PLACEHOLDER IMPLEMENTATION — READ BEFORE USE OR MODIFICATION.
|
||||
*
|
||||
* This class lays out views by direct, procedural arithmetic. It has no
|
||||
* constraint solver. It cannot compress, expand, or negotiate space among
|
||||
* views the way NSStackView does; it only places views at their existing
|
||||
* frame size, in order, separated by fixed spacing.
|
||||
*
|
||||
* Known, accepted limitations:
|
||||
* - `distribution` is stored but has no effect on layout. Fill,
|
||||
* FillEqually, FillProportionally, and EqualSpacing are unimplemented.
|
||||
* - Center-gravity views are clamped against the Leading edge only; if
|
||||
* Leading + Center + Trailing content overflows the container, Center
|
||||
* views can overlap Trailing views instead of compressing.
|
||||
* - `visibilityPriority:forView:` only supports the two extreme values
|
||||
* (MustHold / NotVisible). Intermediate priorities are accepted but
|
||||
* have no defined effect.
|
||||
* - No guarantee is made of correctness beyond what a single manual
|
||||
* test (Tests/Manual/CPStackViewTest) exercises: orientation, the
|
||||
* three gravity areas, alignment switching, spacing, and hidden-view
|
||||
* detachment. Insertion, removal, custom spacing, and the CPCoding
|
||||
* archive path are implemented but not verified by that test.
|
||||
*
|
||||
* This exists to give AppKit a working CPStackView symbol now, not to be
|
||||
* a durable design. It is expected to be replaced by a constraint-solver
|
||||
* based implementation (Kiwi.js) when time permits. Do not build on its
|
||||
* internal layout algorithm as if it were a stable foundation.
|
||||
*/
|
||||
|
||||
@import "CPView.j"
|
||||
@import <Foundation/CPMapTable.j>
|
||||
|
||||
// MARK: -
|
||||
// MARK: Minimal local type definitions
|
||||
//
|
||||
// These types support this file only. They are not shared with the rest
|
||||
// of AppKit. A future constraint-solver based Auto Layout engine will
|
||||
// replace them. Numeric values match the equivalent Cocoa constants
|
||||
// (NSUserInterfaceLayoutOrientation, NSLayoutAttribute) so that a later,
|
||||
// solver-based CPLayoutAttribute can reuse these numbers without a
|
||||
// renumbering pass.
|
||||
|
||||
@typedef CPUserInterfaceLayoutOrientation
|
||||
CPUserInterfaceLayoutOrientationHorizontal = 0;
|
||||
CPUserInterfaceLayoutOrientationVertical = 1;
|
||||
|
||||
@typedef CPLayoutAttribute
|
||||
CPLayoutAttributeLeft = 1;
|
||||
CPLayoutAttributeRight = 2;
|
||||
CPLayoutAttributeTop = 3;
|
||||
CPLayoutAttributeBottom = 4;
|
||||
CPLayoutAttributeLeading = 5;
|
||||
CPLayoutAttributeTrailing = 6;
|
||||
CPLayoutAttributeWidth = 7;
|
||||
CPLayoutAttributeHeight = 8;
|
||||
CPLayoutAttributeCenterX = 9;
|
||||
CPLayoutAttributeCenterY = 10;
|
||||
|
||||
@typedef CPEdgeInsets
|
||||
|
||||
/*!
|
||||
Creates a CPEdgeInsets. Argument order matches Cocoa's NSEdgeInsetsMake
|
||||
(top, left, bottom, right). Storage reuses the existing CGInset struct,
|
||||
whose field order is (top, right, bottom, left).
|
||||
*/
|
||||
function CPEdgeInsetsMake(top, left, bottom, right)
|
||||
{
|
||||
return CGInsetMake(top, right, bottom, left);
|
||||
}
|
||||
|
||||
function CPEdgeInsetsEqualToEdgeInsets(lhsInsets, rhsInsets)
|
||||
{
|
||||
return CGInsetEqualToInset(lhsInsets, rhsInsets);
|
||||
}
|
||||
|
||||
// Gravity Areas
|
||||
@typedef CPStackViewGravity
|
||||
@@ -60,6 +133,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
{
|
||||
CPUserInterfaceLayoutOrientation _orientation;
|
||||
CPLayoutAttribute _alignment;
|
||||
CPStackViewDistribution _distribution;
|
||||
float _spacing;
|
||||
CPEdgeInsets _edgeInsets;
|
||||
|
||||
@@ -99,6 +173,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
{
|
||||
_orientation = CPUserInterfaceLayoutOrientationHorizontal;
|
||||
_alignment = CPLayoutAttributeCenterY; // Default alignment
|
||||
_distribution = CPStackViewDistributionGravityAreas;
|
||||
_spacing = 8.0; // Default Cocoa spacing
|
||||
_edgeInsets = CPEdgeInsetsMake(0, 0, 0, 0);
|
||||
_detachesHiddenViews = YES;
|
||||
@@ -169,6 +244,27 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
The distribution mode for the stack view.
|
||||
@note Not yet applied to layout. All views are laid out at their
|
||||
existing frame size regardless of this value, pending the
|
||||
constraint-solver based layout engine. The value is stored and
|
||||
returned so client code can read back what was set.
|
||||
*/
|
||||
- (CPStackViewDistribution)distribution
|
||||
{
|
||||
return _distribution;
|
||||
}
|
||||
|
||||
- (void)setDistribution:(CPStackViewDistribution)aDistribution
|
||||
{
|
||||
if (_distribution === aDistribution)
|
||||
return;
|
||||
|
||||
_distribution = aDistribution;
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
/*!
|
||||
The minimum spacing, in points, between adjacent views in the stack view.
|
||||
*/
|
||||
@@ -233,6 +329,21 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
return _viewsLeading; // Leading or Top
|
||||
}
|
||||
|
||||
/*!
|
||||
Rebuilds _arrangedSubviews from the three gravity containers, in
|
||||
Leading, Center, Trailing order. Call after any change to a gravity
|
||||
container so _arrangedSubviews stays a correct, single source of truth
|
||||
for ordering, rather than an incrementally and separately maintained
|
||||
(and error-prone) copy.
|
||||
*/
|
||||
- (void)_rebuildArrangedSubviews
|
||||
{
|
||||
_arrangedSubviews = [[CPMutableArray alloc] init];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsLeading];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsCenter];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsTrailing];
|
||||
}
|
||||
|
||||
/*!
|
||||
Adds a view to the end of the stack view gravity area.
|
||||
*/
|
||||
@@ -245,7 +356,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
[self removeView:aView];
|
||||
|
||||
[container addObject:aView];
|
||||
[_arrangedSubviews addObject:aView];
|
||||
[self _rebuildArrangedSubviews];
|
||||
|
||||
// Add as actual subview
|
||||
if ([aView superview] !== self)
|
||||
@@ -269,7 +380,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
else
|
||||
[container insertObject:aView atIndex:index];
|
||||
|
||||
[_arrangedSubviews addObject:aView];
|
||||
[self _rebuildArrangedSubviews];
|
||||
|
||||
if ([aView superview] !== self)
|
||||
[self addSubview:aView];
|
||||
@@ -284,13 +395,9 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
{
|
||||
var container = [self _containerForGravity:gravity];
|
||||
|
||||
// Remove old views from arranged list and superview
|
||||
// Remove old views from superview
|
||||
for (var i = 0; i < [container count]; i++)
|
||||
{
|
||||
var oldView = container[i];
|
||||
[oldView removeFromSuperview];
|
||||
[_arrangedSubviews removeObject:oldView];
|
||||
}
|
||||
[container[i] removeFromSuperview];
|
||||
|
||||
[container removeAllObjects];
|
||||
|
||||
@@ -298,10 +405,10 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
{
|
||||
var newView = views[i];
|
||||
[container addObject:newView];
|
||||
[_arrangedSubviews addObject:newView];
|
||||
[self addSubview:newView];
|
||||
}
|
||||
|
||||
[self _rebuildArrangedSubviews];
|
||||
[self setNeedsLayout:YES];
|
||||
}
|
||||
|
||||
@@ -316,7 +423,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
[_viewsLeading removeObject:aView];
|
||||
[_viewsCenter removeObject:aView];
|
||||
[_viewsTrailing removeObject:aView];
|
||||
[_arrangedSubviews removeObject:aView];
|
||||
[self _rebuildArrangedSubviews];
|
||||
|
||||
[aView removeFromSuperview];
|
||||
|
||||
@@ -531,6 +638,13 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
var limit = (dir === 1) ? count : -1;
|
||||
var step = (dir === 1) ? 1 : -1;
|
||||
|
||||
// Spacing is applied as a gap *before* placing an element (except the
|
||||
// first placed one), rather than trailing off the end after the last
|
||||
// element. This keeps the returned cursor at the true content edge,
|
||||
// with no phantom spacing past the final view.
|
||||
var hasPlacedAny = false;
|
||||
var pendingSpacing = 0;
|
||||
|
||||
for (; i !== limit; i += step)
|
||||
{
|
||||
var view = views[i];
|
||||
@@ -538,6 +652,9 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
if (_detachesHiddenViews && [view isHidden])
|
||||
continue;
|
||||
|
||||
if (hasPlacedAny)
|
||||
cursor += (dir === 1) ? pendingSpacing : -pendingSpacing;
|
||||
|
||||
var viewFrame = [view frame];
|
||||
var viewSizePrimary = isVert ? CGRectGetHeight(viewFrame) : CGRectGetWidth(viewFrame);
|
||||
|
||||
@@ -617,11 +734,10 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
|
||||
if (dir === 1) {
|
||||
originY = cursor;
|
||||
cursor += sizeH + [self _spacingAfterView:view];
|
||||
cursor += sizeH;
|
||||
} else {
|
||||
cursor -= sizeH;
|
||||
originY = cursor;
|
||||
cursor -= [self _spacingAfterView:view];
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -633,15 +749,17 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
|
||||
if (dir === 1) {
|
||||
originX = cursor;
|
||||
cursor += sizeW + [self _spacingAfterView:view];
|
||||
cursor += sizeW;
|
||||
} else {
|
||||
cursor -= sizeW;
|
||||
originX = cursor;
|
||||
cursor -= [self _spacingAfterView:view];
|
||||
}
|
||||
}
|
||||
|
||||
[view setFrame:CGRectMake(originX, originY, sizeW, sizeH)];
|
||||
|
||||
pendingSpacing = [self _spacingAfterView:view];
|
||||
hasPlacedAny = true;
|
||||
}
|
||||
|
||||
return cursor;
|
||||
@@ -657,6 +775,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
{
|
||||
_orientation = [aCoder decodeIntForKey:@"CPStackViewOrientation"];
|
||||
_alignment = [aCoder decodeIntForKey:@"CPStackViewAlignment"];
|
||||
_distribution = [aCoder decodeIntForKey:@"CPStackViewDistribution"];
|
||||
_spacing = [aCoder decodeFloatForKey:@"CPStackViewSpacing"];
|
||||
_edgeInsets = [aCoder decodeObjectForKey:@"CPStackViewEdgeInsets"]; // Assuming CPEdgeInsets supports obj coding or manual decode
|
||||
if (!_edgeInsets) _edgeInsets = CPEdgeInsetsMake(0,0,0,0);
|
||||
@@ -668,10 +787,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
_viewsTrailing = [aCoder decodeObjectForKey:@"CPStackViewViewsTrailing"] || [];
|
||||
|
||||
// Rebuild arranged subviews cache
|
||||
_arrangedSubviews = [[CPMutableArray alloc] init];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsLeading];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsCenter];
|
||||
[_arrangedSubviews addObjectsFromArray:_viewsTrailing];
|
||||
[self _rebuildArrangedSubviews];
|
||||
|
||||
_customSpacings = [aCoder decodeObjectForKey:@"CPStackViewCustomSpacings"] || [[CPMapTable alloc] init];
|
||||
_visibilityPriorities = [[CPMapTable alloc] init]; // usually not persisted
|
||||
@@ -684,6 +800,7 @@ var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
|
||||
[super encodeWithCoder:aCoder];
|
||||
[aCoder encodeInt:_orientation forKey:@"CPStackViewOrientation"];
|
||||
[aCoder encodeInt:_alignment forKey:@"CPStackViewAlignment"];
|
||||
[aCoder encodeInt:_distribution forKey:@"CPStackViewDistribution"];
|
||||
[aCoder encodeFloat:_spacing forKey:@"CPStackViewSpacing"];
|
||||
[aCoder encodeObject:_edgeInsets forKey:@"CPStackViewEdgeInsets"];
|
||||
[aCoder encodeBool:_detachesHiddenViews forKey:@"CPStackViewDetachesHiddenViews"];
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
@class CPLayoutManager
|
||||
@class CPTextContainer
|
||||
@class CPFontManager
|
||||
@class _CPFontPanelPreviewView
|
||||
|
||||
/*
|
||||
Collection indexes
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName";
|
||||
|
||||
// Define missing global tab stop type constants
|
||||
@typedef CPTabStopType
|
||||
CPLeftTabStopType = 0;
|
||||
CPRightTabStopType = 1;
|
||||
CPCenterTabStopType = 2;
|
||||
|
||||
@@ -29,13 +29,15 @@
|
||||
@import "CPMenuItem.j"
|
||||
|
||||
// Orientations matching AppKit standards
|
||||
// typedef enum CPRulerOrientation
|
||||
CPHorizontalRuler = 0,
|
||||
CPVerticalRuler = 1,
|
||||
CPRulerOrientationHorizontal = 0,
|
||||
CPRulerOrientationVertical = 1
|
||||
@typedef CPRulerOrientation
|
||||
CPHorizontalRuler = 0;
|
||||
CPVerticalRuler = 1;
|
||||
CPRulerOrientationHorizontal = 0;
|
||||
CPRulerOrientationVertical = 1;
|
||||
|
||||
@class CPRulerView;
|
||||
@class CPScrollView;
|
||||
@class CPTextTab;
|
||||
|
||||
|
||||
// MARK: - CPRulerMarker (Interactive Handles with Dynamic Alignment Icons)
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
*/
|
||||
|
||||
@import "CPView.j"
|
||||
@class CPTextView;
|
||||
@class CPTextContainer;
|
||||
@import "CPTextField.j"
|
||||
@import <Foundation/CPAttributedString.j>
|
||||
|
||||
@class CPTextView;
|
||||
|
||||
@implementation _CPTableTextAttachment : CPView
|
||||
{
|
||||
CPArray _headers;
|
||||
|
||||
+291
-66
@@ -17,17 +17,38 @@
|
||||
*
|
||||
* 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
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPIndexPath.j>
|
||||
@import <Foundation/CPArray.j>
|
||||
|
||||
/*
|
||||
* CPTreeNode implements the NSTreeNode contract.
|
||||
* The _childNodes array contains only CPTreeNode instances.
|
||||
* The representedObject property contains the application data.
|
||||
* The _parentNode and _childNodes properties maintain a strict bidirectional relationship.
|
||||
* The KVC mutation methods are the public mechanism to change the tree structure.
|
||||
*/
|
||||
@implementation CPTreeNode : CPObject
|
||||
{
|
||||
id _representedObject @accessors(property=representedObject);
|
||||
CPTreeNode _parentNode @accessors(property=parentNode);
|
||||
/*
|
||||
* KVO notifications on childNodes and parentNode are not reliable during
|
||||
* internal structural moves. insertObject:inChildNodesAtIndex: and
|
||||
* replaceObjectInChildNodesAtIndex:withObject: detach a node from its
|
||||
* prior position by mutating _childNodes directly, or through
|
||||
* _removeChildNode:, bypassing the KVC proxy for that step. An observer
|
||||
* of the old parent's childNodes, or of a moved node's parentNode, can
|
||||
* miss the change entirely or see it reported as the wrong kind of
|
||||
* change. This is a deliberate trade against the cost of routing every
|
||||
* internal detach through the proxy. Do not rely on these two
|
||||
* properties for observation during a move; rely only on the state
|
||||
* after the call returns.
|
||||
*/
|
||||
id _representedObject @accessors(readonly, property=representedObject);
|
||||
CPTreeNode _parentNode @accessors(readonly, property=parentNode);
|
||||
CPMutableArray _childNodes;
|
||||
}
|
||||
|
||||
@@ -49,27 +70,108 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
/*
|
||||
* Route plain init through the designated initializer.
|
||||
* Without this override, [[CPTreeNode alloc] init] leaves _childNodes unset.
|
||||
* The first mutation call then fails against an undefined array.
|
||||
*/
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithRepresentedObject:nil];
|
||||
}
|
||||
|
||||
/*
|
||||
* Return YES if adding aTreeNode below self makes a cycle.
|
||||
* This method walks the parent chain.
|
||||
* The operation time is proportional to the tree depth.
|
||||
*/
|
||||
- (BOOL)_wouldCreateCycleWithNode:(CPTreeNode)aTreeNode
|
||||
{
|
||||
for (var node = self; node; node = node._parentNode)
|
||||
{
|
||||
if (node === aTreeNode)
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
/*
|
||||
* Enforce the NSTreeNode abstraction boundary.
|
||||
* All children must be CPTreeNode instances.
|
||||
*/
|
||||
- (void)_validateChildNode:(id)aTreeNode
|
||||
{
|
||||
if (![aTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
{
|
||||
[CPException raise:CPInvalidArgumentException
|
||||
reason:"CPTreeNode children must be CPTreeNode instances."];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove a child node directly.
|
||||
* This method bypasses the KVC proxy methods.
|
||||
* Use this method for internal structural changes to prevent KVO overhead.
|
||||
*/
|
||||
- (void)_removeChildNode:(CPTreeNode)aNode
|
||||
{
|
||||
var index = [_childNodes indexOfObjectIdenticalTo:aNode];
|
||||
|
||||
/*
|
||||
* A caller reaches this method only when aNode.parentNode already equals
|
||||
* self (see the two call sites below). If self._childNodes does not
|
||||
* actually contain aNode at that point, the parent/child relationship
|
||||
* is already broken. indexPath raises for this identical class of
|
||||
* inconsistency; silently returning here would hide the same problem
|
||||
* instead of surfacing it.
|
||||
*/
|
||||
if (index === CPNotFound)
|
||||
{
|
||||
[CPException raise:CPInternalInconsistencyException
|
||||
reason:"CPTreeNode parent and child relationship is inconsistent."];
|
||||
}
|
||||
|
||||
aNode._parentNode = nil;
|
||||
[_childNodes removeObjectAtIndex:index];
|
||||
}
|
||||
|
||||
- (CPIndexPath)indexPath
|
||||
{
|
||||
// If we have a parent, calculate path based on parent's path + our index
|
||||
if (_parentNode)
|
||||
if (!_parentNode)
|
||||
return [CPIndexPath indexPathWithIndexes:[]];
|
||||
|
||||
var indexes = [],
|
||||
node = self;
|
||||
|
||||
while (node._parentNode)
|
||||
{
|
||||
// Search the parent's child nodes, not our own!
|
||||
var index = [[_parentNode childNodes] indexOfObjectIdenticalTo:self];
|
||||
|
||||
// If the parent is the root (and technically has no path itself in some implementations),
|
||||
// we might get nil. Handle that gracefully.
|
||||
var parentPath = [_parentNode indexPath];
|
||||
|
||||
if (parentPath)
|
||||
return [parentPath indexPathByAddingIndex:index];
|
||||
|
||||
return [CPIndexPath indexPathWithIndex:index];
|
||||
var parent = node._parentNode,
|
||||
index = [parent._childNodes indexOfObjectIdenticalTo:node];
|
||||
|
||||
if (index === CPNotFound)
|
||||
{
|
||||
[CPException raise:CPInternalInconsistencyException
|
||||
reason:"CPTreeNode parent and child relationship is inconsistent."];
|
||||
}
|
||||
|
||||
[indexes addObject:index];
|
||||
node = parent;
|
||||
}
|
||||
|
||||
// If we are the root, we don't have an index path in the context of a tree controller usually,
|
||||
// or we are [] (empty path). Returning nil is acceptable for the absolute root.
|
||||
return nil;
|
||||
|
||||
/*
|
||||
* indexes was collected leaf-to-root. Build a second array in
|
||||
* root-to-leaf order by walking indexes backward. CPArray has no
|
||||
* -reverse selector; count/objectAtIndex:/addObject: are the verified,
|
||||
* already-used-elsewhere primitives.
|
||||
*/
|
||||
var orderedIndexes = [],
|
||||
count = [indexes count];
|
||||
|
||||
while (count--)
|
||||
[orderedIndexes addObject:[indexes objectAtIndex:count]];
|
||||
|
||||
return [CPIndexPath indexPathWithIndexes:orderedIndexes];
|
||||
}
|
||||
|
||||
- (BOOL)isLeaf
|
||||
@@ -79,7 +181,10 @@
|
||||
|
||||
- (CPArray)childNodes
|
||||
{
|
||||
// Return a copy to prevent external modification without KVC
|
||||
/*
|
||||
* Return a copy.
|
||||
* This prevents external changes that bypass the KVC methods.
|
||||
*/
|
||||
return [_childNodes copy];
|
||||
}
|
||||
|
||||
@@ -88,78 +193,182 @@
|
||||
return [self mutableArrayValueForKey:@"childNodes"];
|
||||
}
|
||||
|
||||
// MARK: - KVC Compliance Methods
|
||||
/*
|
||||
* KVC compliance methods.
|
||||
* The mutableArrayValueForKey: method uses these names.
|
||||
*/
|
||||
|
||||
- (void)insertObject:(CPTreeNode)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
// Optional: Auto-detach from old parent if strictly moving nodes
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]] && aTreeNode._parentNode)
|
||||
var count = [_childNodes count];
|
||||
|
||||
if (anIndex < 0 || anIndex > count)
|
||||
{
|
||||
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
|
||||
[CPException raise:CPRangeException
|
||||
reason:"index (" + anIndex + ") beyond bounds (0 .. " + count + ") for insertObject:inChildNodesAtIndex:"];
|
||||
}
|
||||
|
||||
// Direct ivar access is allowed here since we are inside the class implementation
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
aTreeNode._parentNode = self;
|
||||
[self _validateChildNode:aTreeNode];
|
||||
|
||||
if ([self _wouldCreateCycleWithNode:aTreeNode])
|
||||
{
|
||||
[CPException raise:CPInvalidArgumentException
|
||||
reason:"Inserting a CPTreeNode beneath itself or one of its descendants makes a cycle."];
|
||||
}
|
||||
|
||||
/*
|
||||
* Detach the node from its old parent first.
|
||||
* The code validated the index before this change.
|
||||
*/
|
||||
if (aTreeNode._parentNode)
|
||||
{
|
||||
if (aTreeNode._parentNode === self)
|
||||
{
|
||||
var originalIndex = [_childNodes indexOfObjectIdenticalTo:aTreeNode];
|
||||
|
||||
/*
|
||||
* Bypass KVO for this internal structural adjustment.
|
||||
*/
|
||||
aTreeNode._parentNode = nil;
|
||||
[_childNodes removeObjectAtIndex:originalIndex];
|
||||
|
||||
/*
|
||||
* The removal shifts the array elements.
|
||||
* Adjust the target index to maintain the position relative to the original array.
|
||||
* This matches the Cocoa move semantics.
|
||||
*/
|
||||
if (originalIndex < anIndex)
|
||||
--anIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* Detach the node from its old parent.
|
||||
* Use the internal method to bypass KVO overhead.
|
||||
*/
|
||||
[aTreeNode._parentNode _removeChildNode:aTreeNode];
|
||||
}
|
||||
}
|
||||
|
||||
aTreeNode._parentNode = self;
|
||||
[_childNodes insertObject:aTreeNode atIndex:anIndex];
|
||||
}
|
||||
|
||||
- (void)removeObjectFromChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
var node = [_childNodes objectAtIndex:anIndex];
|
||||
|
||||
if ([node isKindOfClass:[CPTreeNode class]])
|
||||
node._parentNode = nil;
|
||||
|
||||
node._parentNode = nil;
|
||||
[_childNodes removeObjectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
- (void)replaceObjectFromChildNodesAtIndex:(CPInteger)anIndex withObject:(id)aTreeNode
|
||||
- (void)replaceObjectInChildNodesAtIndex:(CPInteger)anIndex withObject:(CPTreeNode)aTreeNode
|
||||
{
|
||||
var oldTreeNode = [_childNodes objectAtIndex:anIndex];
|
||||
|
||||
if ([oldTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
oldTreeNode._parentNode = nil;
|
||||
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
aTreeNode._parentNode = self;
|
||||
[self _validateChildNode:aTreeNode];
|
||||
|
||||
if (oldTreeNode === aTreeNode)
|
||||
return;
|
||||
|
||||
if ([self _wouldCreateCycleWithNode:aTreeNode])
|
||||
{
|
||||
[CPException raise:CPInvalidArgumentException
|
||||
reason:"Replacing a child with itself or one of its ancestors makes a cycle."];
|
||||
}
|
||||
|
||||
/*
|
||||
* If the replacement node is already a child of this parent, remove it first.
|
||||
* The removal shifts the array elements.
|
||||
* Adjust the target index before the replace operation.
|
||||
* This matches the Cocoa KVC mutation semantics.
|
||||
*/
|
||||
var oldParent = aTreeNode._parentNode;
|
||||
|
||||
if (oldParent === self)
|
||||
{
|
||||
var replacementIndex = [_childNodes indexOfObjectIdenticalTo:aTreeNode];
|
||||
|
||||
/*
|
||||
* aTreeNode.parentNode already equals self at this point. If
|
||||
* self._childNodes does not actually contain aTreeNode, the
|
||||
* parent/child relationship is already broken. indexPath raises
|
||||
* for this identical class of inconsistency; proceeding here would
|
||||
* silently tolerate the same problem instead of surfacing it.
|
||||
*/
|
||||
if (replacementIndex === CPNotFound)
|
||||
{
|
||||
[CPException raise:CPInternalInconsistencyException
|
||||
reason:"CPTreeNode parent and child relationship is inconsistent."];
|
||||
}
|
||||
|
||||
/*
|
||||
* Bypass KVO for this internal structural adjustment.
|
||||
*/
|
||||
aTreeNode._parentNode = nil;
|
||||
[_childNodes removeObjectAtIndex:replacementIndex];
|
||||
|
||||
if (replacementIndex < anIndex)
|
||||
--anIndex;
|
||||
}
|
||||
else if (oldParent)
|
||||
{
|
||||
/*
|
||||
* Detach the node from its old parent.
|
||||
* Use the internal method to bypass KVO overhead.
|
||||
*/
|
||||
[oldParent _removeChildNode:aTreeNode];
|
||||
}
|
||||
|
||||
oldTreeNode._parentNode = nil;
|
||||
aTreeNode._parentNode = self;
|
||||
|
||||
[_childNodes replaceObjectAtIndex:anIndex withObject:aTreeNode];
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
- (id)objectInChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
return [_childNodes objectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
- (CPInteger)count
|
||||
- (CPInteger)countOfChildNodes
|
||||
{
|
||||
return [_childNodes count];
|
||||
}
|
||||
|
||||
- (id)objectAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
return [_childNodes objectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
// MARK: - Utility
|
||||
|
||||
- (void)sortWithSortDescriptors:(CPArray)sortDescriptors recursively:(BOOL)shouldSortRecursively
|
||||
{
|
||||
[_childNodes sortUsingDescriptors:sortDescriptors];
|
||||
|
||||
if (!shouldSortRecursively)
|
||||
return;
|
||||
|
||||
var count = [_childNodes count];
|
||||
while (count--)
|
||||
{
|
||||
var child = [_childNodes objectAtIndex:count];
|
||||
if ([child respondsToSelector:@selector(sortWithSortDescriptors:recursively:)])
|
||||
[child sortWithSortDescriptors:sortDescriptors recursively:YES];
|
||||
[_childNodes sortUsingDescriptors:sortDescriptors];
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use an explicit stack, not recursion.
|
||||
* The recursive form is not a tail call.
|
||||
* The sibling loop continues after each child call returns.
|
||||
* Only JavaScriptCore performs tail call optimization.
|
||||
* The explicit stack prevents stack overflow on deep trees.
|
||||
*/
|
||||
var stack = [];
|
||||
|
||||
[stack addObject:self];
|
||||
|
||||
while ([stack count])
|
||||
{
|
||||
var node = [stack lastObject];
|
||||
|
||||
[stack removeLastObject];
|
||||
|
||||
[node._childNodes sortUsingDescriptors:sortDescriptors];
|
||||
|
||||
var count = [node._childNodes count];
|
||||
|
||||
while (count--)
|
||||
{
|
||||
[stack addObject:[node._childNodes objectAtIndex:count]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,17 +378,17 @@
|
||||
return self;
|
||||
|
||||
var node = self,
|
||||
length = [indexPath length];
|
||||
length = [indexPath length];
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var index = [indexPath indexAtPosition:i],
|
||||
count = [node count];
|
||||
|
||||
if (index >= count || index < 0)
|
||||
count = [node countOfChildNodes];
|
||||
|
||||
if (index < 0 || index >= count)
|
||||
return nil;
|
||||
|
||||
node = [node objectAtIndex:index];
|
||||
|
||||
node = [node objectInChildNodesAtIndex:index];
|
||||
}
|
||||
|
||||
return node;
|
||||
@@ -187,7 +396,6 @@
|
||||
|
||||
@end
|
||||
|
||||
// Coding implementation remains correct
|
||||
var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
|
||||
CPTreeNodeParentNodeKey = @"CPTreeNodeParentNodeKey",
|
||||
CPTreeNodeChildNodesKey = @"CPTreeNodeChildNodesKey";
|
||||
@@ -203,10 +411,27 @@ var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
|
||||
_representedObject = [aCoder decodeObjectForKey:CPTreeNodeRepresentedObjectKey];
|
||||
_parentNode = [aCoder decodeObjectForKey:CPTreeNodeParentNodeKey];
|
||||
_childNodes = [aCoder decodeObjectForKey:CPTreeNodeChildNodesKey];
|
||||
|
||||
// Safety check to ensure decoding gave us a CPArray
|
||||
|
||||
if (!_childNodes)
|
||||
_childNodes = [[CPMutableArray alloc] init];
|
||||
_childNodes = [];
|
||||
|
||||
if (![_childNodes isKindOfClass:[CPMutableArray class]])
|
||||
_childNodes = [_childNodes mutableCopy];
|
||||
|
||||
/*
|
||||
* The child array is the authoritative structure.
|
||||
* Re-establish the parent links.
|
||||
* This makes the decoded tree match the tree built by the mutation methods.
|
||||
*/
|
||||
var count = [_childNodes count];
|
||||
|
||||
while (count--)
|
||||
{
|
||||
var child = [_childNodes objectAtIndex:count];
|
||||
|
||||
[self _validateChildNode:child];
|
||||
child._parentNode = self;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
|
||||
@@ -481,7 +481,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
_DOMWindow.addEventListener("blur", onBlurEventCallback, NO);
|
||||
_DOMWindow.addEventListener("focus", onFocusEventCallback, NO);
|
||||
|
||||
_DOMWindow.addEventListener("unload", function()
|
||||
_DOMWindow.addEventListener("pagehide", function()
|
||||
{
|
||||
_DOMWindow.removeEventListener("unload", arguments.callee, NO);
|
||||
|
||||
|
||||
+18
-4
@@ -105,17 +105,31 @@
|
||||
// MARK: Creating a Dictionary Representation
|
||||
|
||||
/*!
|
||||
Returns a dictionary representation of the map table.
|
||||
Note: This will only work correctly if all keys are strings.
|
||||
Returns a dictionary representation of the map table.
|
||||
Note: This will only work correctly if all keys are strings.
|
||||
|
||||
@return A CPDictionary containing the entries of the map table.
|
||||
@return A CPDictionary containing the entries of the map table.
|
||||
*/
|
||||
- (CPDictionary)dictionaryRepresentation
|
||||
{
|
||||
var dictionary = [CPDictionary dictionary];
|
||||
|
||||
for (var [key, value] of _map.entries())
|
||||
// TODO: Revert to ES6 destructuring in the loop declaration once the
|
||||
// legacy compiler is retired. The legacy parser fails to recognize
|
||||
// `var [key, value]` as a local scope declaration, causing the variables
|
||||
// to leak to the global object and emitting false-positive "uninitialized
|
||||
// global variable" warnings, which is unacceptable for CI hygiene.
|
||||
//
|
||||
// for (var [key, value] of _map.entries())
|
||||
// {
|
||||
// [dictionary setObject:value forKey:key];
|
||||
// }
|
||||
|
||||
for (var entry of _map.entries())
|
||||
{
|
||||
var key = entry[0],
|
||||
value = entry[1];
|
||||
|
||||
[dictionary setObject:value forKey:key];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,32 @@
|
||||
# Cappuccino: Build Desktop-Class Web Applications
|
||||
|
||||
> **✨ Project Status: v1.5.0 Baseline & Upcoming v2.0.0 Toolchain**
|
||||
>
|
||||
> Cappuccino has been under continuous development since 2008 and is actively maintained. This v1.5.0 release establishes the new resolution-independent Aristo3 theme as the baseline. While Aristo2 remains included, using it requires explicitly setting the theme descriptor.
|
||||
>
|
||||
> Cappuccino has been under continuous development since 2008 and is actively maintained. The v1.5.0 release establishes a baseline as we move to the new resolution-independent Aristo3 theme and Go-based toolchain.
|
||||
|
||||
> For users seeking a complication-free alternative who wish to avoid the Aristo3 work entirely, the legacy-1.4.0 branch provides an unambiguous freeze point. Please note, however, that this legacy branch is not guaranteed to receive any future bug fixes or improvements.
|
||||
|
||||
> Active development is now focused on the upcoming v2.0.0 release, which will transition the full toolchain to Golang and the platform-native binaries it produces, leaving Node.js and npm behind.
|
||||
|
||||
> **🚨 Aristo3 Theme: Community Testing Required**
|
||||
>
|
||||
> Aristo3 represents a non-trivial refactoring of AppKit UI classes. While unit tests pass, it must be tested against real-world applications before being merged into `main`. Community members testing against their own applications will accelerate the process. We are not concerned with minor visual breakages or cosmetic regressions at this stage; **the primary concern is structural failures.**
|
||||
>
|
||||
> The fully merged state has been pushed to the `aristo3` branch on the canonical repo.
|
||||
>
|
||||
> **How to help:**
|
||||
> 1. Check out the `aristo3` branch: `git fetch origin && git checkout aristo3`
|
||||
> 2. Build the frameworks and run your existing applications against this branch.
|
||||
> 3. Open your browser's developer console and watch for:
|
||||
> - Uncaught `CPException`s or JavaScript errors.
|
||||
> - Infinite layout loops (browser freezing/tab crashing).
|
||||
> - Broken responder chains or keyboard event handling.
|
||||
> - KVO/Binding failures or `valueForThemeAttribute:` resolving to `nil` unexpectedly.
|
||||
> - View hierarchy corruption (subviews disappearing or failing to clip).
|
||||
>
|
||||
> Please leave a 👍 on the [Aristo3 Pull Request](https://github.com/cappuccino/cappuccino/pull/3038) if your apps run without structural failures. If you encounter exceptions or crashes, please leave a comment with the stack trace. **A solid response of thumbs up is required before `main` can be merged. Our intention is to leave no community member behind.**
|
||||
|
||||
|
||||
## Why Use Cappuccino?
|
||||
|
||||
Cappuccino is for building **applications** in the browser — especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits:
|
||||
@@ -93,7 +112,7 @@ Pure JavaScript and Objective-J can be mixed and matched, even in the same file.
|
||||
|
||||
## Frequently Asked Questions (FAQ)
|
||||
|
||||
**Q: What are the advantages over React or Vue?**
|
||||
**Q: What are the advantages over React or Vue?**
|
||||
**A:** React and Vue are excellent libraries for building web UIs. Cappuccino is a comprehensive **framework** for building entire **applications**. It provides a fully integrated stack—including a mature UI library, event handling, and data management—designed for large-scale development.
|
||||
|
||||
Beyond this, Cappuccino provides a more integrated and powerful data-binding layer inspired directly by Cocoa, which dramatically reduces boilerplate code for complex UIs as you can see in this [example code](https://github.com/daboe01/UIBuilder/tree/master/public/Frontend) that uses these features:
|
||||
@@ -103,19 +122,19 @@ Beyond this, Cappuccino provides a more integrated and powerful data-binding lay
|
||||
* **Advanced Filtering with Predicates:** A table displaying thousands of items can be filtered simply by setting a predicate (a declarative filter rule, e.g., `lastName BEGINSWITH 'S'`) on its controller. The UI updates instantly. This eliminates tons of manual state management and filtering logic code.
|
||||
* **Automatic Value Transformation:** Data can be easily formatted for display (e.g., dates, currency, booleans to "Yes/No") directly within the binding itself using value transformers, keeping model data pure and view logic minimal.
|
||||
|
||||
**Q: Can Cappuccino be used on Windows/Linux?**
|
||||
**Q: Can Cappuccino be used on Windows/Linux?**
|
||||
**A:** Yes. The development tools run on Node.js and are platform-independent. Applications can be developed on any OS and deployed on any web server.
|
||||
|
||||
**Q: Is Xcode required?**
|
||||
**Q: Is Xcode required?**
|
||||
**A:** No. Any code editor can be used. Xcode offers optional visual development tools for macOS users, but it is not a requirement.
|
||||
|
||||
**Q: Hasn't Apple moved on from Objective-C, making these APIs obsolete?**
|
||||
**Q: Hasn't Apple moved on from Objective-C, making these APIs obsolete?**
|
||||
**A:** While Swift is Apple's newer language, Objective-C and AppKit remain foundational, actively supported technologies used in many of Apple's flagship applications. Cappuccino leverages the stability and power of this time-tested API design, which is independent of Apple's future product roadmap.
|
||||
|
||||
**Q: How can custom HTML, CSS, or JavaScript libraries be integrated?**
|
||||
**Q: How can custom HTML, CSS, or JavaScript libraries be integrated?**
|
||||
**A:** Cappuccino abstracts away the DOM, but other web technologies can still be integrated. The `CPWebView` control allows arbitrary HTML/CSS/JS content to be embedded. Since Objective-J is a superset of JavaScript, JS libraries can be used and JS functions can be called directly from Objective-J code.
|
||||
|
||||
**Q: Does the LGPL license permit closed-source commercial applications?**
|
||||
**Q: Does the LGPL license permit closed-source commercial applications?**
|
||||
**A:** Yes. The LGPLv2 license allows proprietary, closed-source applications to be built and distributed using Cappuccino. Sharing of source code is only required for any modifications made **to the Cappuccino framework itself**. The application code remains proprietary.
|
||||
|
||||
---
|
||||
|
||||
@@ -490,8 +490,11 @@
|
||||
var result = [CPMutableDictionary dictionary];
|
||||
|
||||
// Test basic for...of iteration
|
||||
for (var [key, value] of dict)
|
||||
for (var entry of dict)
|
||||
{
|
||||
var key = entry[0],
|
||||
value = entry[1];
|
||||
|
||||
[result setObject:value forKey:key];
|
||||
}
|
||||
|
||||
@@ -514,6 +517,8 @@
|
||||
var iterations = 0;
|
||||
for (var entry of emptyDict)
|
||||
{
|
||||
// Explicitly evaluate the bound variable to satisfy the static analyzer.
|
||||
[entry self];
|
||||
iterations++;
|
||||
}
|
||||
[self assert:0 equals:iterations message:@"for...of on an empty dictionary should not iterate"];
|
||||
|
||||
@@ -428,10 +428,19 @@
|
||||
|
||||
|
||||
// 3. Test on an empty set
|
||||
/*
|
||||
The bound variable must be explicitly read to satisfy the static analyzer.
|
||||
Standard JavaScript idioms for unused variables, such as the `_` identifier
|
||||
or the `void` operator, either fail linting or trigger AST collisions in the
|
||||
legacy Node.js parser. Evaluating the variable via a standard Objective-J
|
||||
message send resolves the warning while preserving parser stability.
|
||||
*/
|
||||
|
||||
var emptySet = [CPSet set];
|
||||
var iterations = 0;
|
||||
for (var entry of emptySet)
|
||||
{
|
||||
[itemsSeen addObject:entry];
|
||||
iterations++;
|
||||
}
|
||||
[self assert:0 equals:iterations message:@"for...of on an empty set should not iterate"];
|
||||
|
||||
@@ -1,94 +1,162 @@
|
||||
/*
|
||||
* Jakefile
|
||||
* CPSplitViewTest
|
||||
* CPStackViewTest
|
||||
*
|
||||
* Created by Alexander Ljungberg on January 27, 2012.
|
||||
* Copyright 2012, WireLoad All rights reserved.
|
||||
* Created by You on August 14, 2026.
|
||||
* Copyright 2026, Your Company All rights reserved.
|
||||
*/
|
||||
|
||||
var ENV = require("system").env,
|
||||
FILE = require("file"),
|
||||
JAKE = require("jake"),
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
var ENV = process.env,
|
||||
task = JAKE.task,
|
||||
FileList = JAKE.FileList,
|
||||
app = require("cappuccino/jake").app,
|
||||
app = CAPPUCCINO.Jake.applicationtask.app,
|
||||
configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug",
|
||||
OS = require("os");
|
||||
OS = require("os"),
|
||||
projectName = "CPStackViewTest";
|
||||
|
||||
app ("CPSplitViewTest", function(task)
|
||||
var buildDir = path.resolve(ENV["BUILD_PATH"] || ENV["CAPP_BUILD"] || "Build");
|
||||
|
||||
app (projectName, function(task)
|
||||
{
|
||||
task.setBuildIntermediatesPath(FILE.join("Build", "CPSplitViewTest.build", configuration));
|
||||
task.setBuildPath(FILE.join("Build", configuration));
|
||||
ENV["OBJJ_INCLUDE_PATHS"] = ["Frameworks"];
|
||||
|
||||
task.setProductName("CPSplitViewTest");
|
||||
task.setIdentifier("com.yourcompany.CPSplitViewTest");
|
||||
if (configuration === "Debug")
|
||||
ENV["OBJJ_INCLUDE_PATHS"] = path.join(ENV["OBJJ_INCLUDE_PATHS"], configuration);
|
||||
|
||||
task.setBuildIntermediatesPath(path.join(buildDir, "CPStackViewTest.build", configuration));
|
||||
task.setBuildPath(path.join(buildDir, configuration));
|
||||
|
||||
task.setProductName("CPStackViewTest");
|
||||
task.setIdentifier("com.yourcompany.CPStackViewTest");
|
||||
task.setVersion("1.0");
|
||||
task.setAuthor("WireLoad");
|
||||
task.setAuthor("Your Company");
|
||||
task.setEmail("feedback @nospam@ yourcompany.com");
|
||||
task.setSummary("CPSplitViewTest");
|
||||
task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**")));
|
||||
task.setSummary("CPStackViewTest");
|
||||
task.setSources(new FileList("**/*.j").exclude(path.join("Build", "**")).exclude(path.join("Frameworks", "Source", "**")));
|
||||
task.setResources(new FileList("Resources/**"));
|
||||
task.setIndexFilePath("index.html");
|
||||
task.setInfoPlistPath("Info.plist");
|
||||
task.setNib2CibFlags("-R Resources/");
|
||||
|
||||
if (configuration === "Debug")
|
||||
task.setCompilerFlags("-DDEBUG -g");
|
||||
task.setCompilerFlags("-DDEBUG -g -S --inline-msg-send");
|
||||
else
|
||||
task.setCompilerFlags("-O");
|
||||
task.setCompilerFlags("-O2");
|
||||
});
|
||||
|
||||
task ("default", ["CPSplitViewTest"], function()
|
||||
task ("default", [projectName], function()
|
||||
{
|
||||
printResults(configuration);
|
||||
});
|
||||
|
||||
task ("build", ["default"]);
|
||||
task ("build", ["default"], function()
|
||||
{
|
||||
updateApplicationSize();
|
||||
});
|
||||
|
||||
task ("debug", function()
|
||||
{
|
||||
ENV["CONFIGURATION"] = "Debug";
|
||||
configuration = ENV["CONFIGURATION"] = "Debug";
|
||||
JAKE.subjake(["."], "build", ENV);
|
||||
});
|
||||
|
||||
task ("release", function()
|
||||
{
|
||||
ENV["CONFIGURATION"] = "Release";
|
||||
configuration = ENV["CONFIGURATION"] = "Release";
|
||||
JAKE.subjake(["."], "build", ENV);
|
||||
});
|
||||
|
||||
task ("run", ["debug"], function()
|
||||
{
|
||||
OS.system(["open", FILE.join("Build", "Debug", "CPSplitViewTest", "index.html")]);
|
||||
});
|
||||
|
||||
task ("run-release", ["release"], function()
|
||||
{
|
||||
OS.system(["open", FILE.join("Build", "Release", "CPSplitViewTest", "index.html")]);
|
||||
});
|
||||
|
||||
task ("deploy", ["release"], function()
|
||||
{
|
||||
FILE.mkdirs(FILE.join("Build", "Deployment", "CPSplitViewTest"));
|
||||
OS.system(["press", "-f", FILE.join("Build", "Release", "CPSplitViewTest"), FILE.join("Build", "Deployment", "CPSplitViewTest")]);
|
||||
printResults("Deployment")
|
||||
});
|
||||
|
||||
task ("desktop", ["release"], function()
|
||||
{
|
||||
FILE.mkdirs(FILE.join("Build", "Desktop", "CPSplitViewTest"));
|
||||
require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "CPSplitViewTest"), FILE.join("Build", "Desktop", "CPSplitViewTest", "CPSplitViewTest.app"));
|
||||
printResults("Desktop")
|
||||
});
|
||||
|
||||
task ("run-desktop", ["desktop"], function()
|
||||
{
|
||||
OS.system([FILE.join("Build", "Desktop", "CPSplitViewTest", "CPSplitViewTest.app", "Contents", "MacOS", "NativeHost"), "-i"]);
|
||||
});
|
||||
|
||||
function printResults(configuration)
|
||||
{
|
||||
print("----------------------------");
|
||||
print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPSplitViewTest"));
|
||||
print("----------------------------");
|
||||
console.log("----------------------------");
|
||||
console.log(configuration+" app built at path: "+path.join(buildDir, configuration, projectName));
|
||||
console.log("----------------------------");
|
||||
}
|
||||
|
||||
function updateApplicationSize()
|
||||
{
|
||||
console.log("Calculating application file sizes...");
|
||||
|
||||
var contents = fs.readFileSync(path.join(buildDir, configuration, projectName, "Info.plist"), { encoding: "utf8" }),
|
||||
format = CFPropertyList.sniffedFormatOfString(contents),
|
||||
plist = CFPropertyList.propertyListFromString(contents),
|
||||
totalBytes = {executable:0, data:0, mhtml:0};
|
||||
|
||||
// Get the size of all framework executables and sprite data
|
||||
var frameworksDir = "Frameworks";
|
||||
|
||||
if (configuration === "Debug")
|
||||
frameworksDir = path.join(frameworksDir, "Debug");
|
||||
|
||||
var frameworks = [];
|
||||
|
||||
if (fs.existsSync(frameworksDir)) {
|
||||
frameworks = fs.readdirSync(frameworksDir);
|
||||
}
|
||||
|
||||
frameworks.forEach(function(framework)
|
||||
{
|
||||
if (framework !== "Source")
|
||||
addBundleFileSizes(path.join(frameworksDir, framework), totalBytes);
|
||||
});
|
||||
|
||||
// Read in the default theme name, and attempt to get its size
|
||||
var themeName = plist.valueForKey("CPDefaultTheme") || "Aristo2",
|
||||
themePath = nil;
|
||||
|
||||
if (themeName === "Aristo" || themeName === "Aristo2")
|
||||
themePath = path.join(frameworksDir, "AppKit", "Resources", themeName + ".blend");
|
||||
else
|
||||
themePath = path.join("Frameworks", "Resources", themeName + ".blend");
|
||||
|
||||
if (fs.existsSync(themePath) && fs.lstatSync(themePath).isDirectory())
|
||||
addBundleFileSizes(themePath, totalBytes);
|
||||
|
||||
// Add sizes for the app
|
||||
addBundleFileSizes(path.join(buildDir, configuration, projectName), totalBytes);
|
||||
|
||||
console.log("Executables: " + totalBytes.executable + ", sprite data: " + totalBytes.data + ", total: " + (totalBytes.executable + totalBytes.data));
|
||||
|
||||
var dict = new CFMutableDictionary();
|
||||
|
||||
dict.setValueForKey("executable", totalBytes.executable);
|
||||
dict.setValueForKey("data", totalBytes.data);
|
||||
dict.setValueForKey("mhtml", totalBytes.mhtml);
|
||||
|
||||
plist.setValueForKey("CPApplicationSize", dict);
|
||||
fs.writeFileSync(path.join(buildDir, configuration, projectName, "Info.plist"), CFPropertyList.stringFromPropertyList(plist, format), { encoding: "utf8" });
|
||||
}
|
||||
|
||||
function addBundleFileSizes(bundlePath, totalBytes)
|
||||
{
|
||||
var bundleName = path.basename(bundlePath),
|
||||
environment = bundleName === "Foundation" ? "Objj" : "Browser",
|
||||
bundlePath = path.join(bundlePath, environment + ".environment");
|
||||
|
||||
if (fs.existsSync(bundlePath) && fs.lstatSync(bundlePath).isDirectory())
|
||||
{
|
||||
var filename = bundleName + ".sj",
|
||||
filePath = path.join(bundlePath, filename);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
totalBytes.executable += fs.lstatSync(filePath).size;
|
||||
}
|
||||
|
||||
filePath = path.join(bundlePath, "dataURLs.txt");
|
||||
|
||||
if (fs.existsSync(filePath))
|
||||
totalBytes.data += fs.lstatSync(filePath).size;
|
||||
|
||||
filePath = path.join(bundlePath, "MHTMLData.txt");
|
||||
|
||||
if (fs.existsSync(filePath))
|
||||
totalBytes.mhtml += fs.lstatSync(filePath).size;
|
||||
|
||||
filePath = path.join(bundlePath, "MHTMLPaths.txt");
|
||||
|
||||
if (fs.existsSync(filePath))
|
||||
totalBytes.mhtml += fs.lstatSync(filePath).size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
@import <Foundation/Foundation.j>
|
||||
|
||||
@implementation CopyAccessorTestClass : CPObject
|
||||
{
|
||||
CPMutableArray ivar @accessors(copy);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CopyAccessorTest : OJTestCase
|
||||
{
|
||||
CopyAccessorTestClass testClass;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
testClass = [[CopyAccessorTestClass alloc] init];
|
||||
}
|
||||
|
||||
- (void)testCopyAccessorStoresDistinctObject
|
||||
{
|
||||
var original = [@"a", @"b"];
|
||||
|
||||
[testClass setIvar:original];
|
||||
|
||||
[self assertFalse:[testClass ivar] === original];
|
||||
}
|
||||
|
||||
- (void)testCopyAccessorIsUnaffectedByLaterMutation
|
||||
{
|
||||
var original = [@"a", @"b"];
|
||||
|
||||
[testClass setIvar:original];
|
||||
[original addObject:@"c"];
|
||||
|
||||
[self assert:2 equals:[[testClass ivar] count]];
|
||||
}
|
||||
|
||||
- (void)testCopyAccessorPreservesContentAtAssignment
|
||||
{
|
||||
var original = [@"a", @"b"];
|
||||
|
||||
[testClass setIvar:original];
|
||||
|
||||
[self assert:original equals:[testClass ivar]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,5 +1,24 @@
|
||||
@import <Foundation/Foundation.j>
|
||||
|
||||
/*
|
||||
* NOTE: INTENTIONAL COMPILER WARNINGS
|
||||
*
|
||||
* The shadowing warnings generated by this file (e.g., "Local declaration
|
||||
* of 'ivar1' hides instance variable") are an expected artifact of the test.
|
||||
*
|
||||
* The objective of these methods is to validate the compiler's code generation
|
||||
* during a shadowing event. In standard Objective-J, unqualified instance
|
||||
* variables are transformed into explicit accessors (e.g., self.ivar1). When a
|
||||
* local variable shares the same identifier, the AST transformation must
|
||||
* intentionally suppress this behavior to maintain proper lexical scope.
|
||||
*
|
||||
* Modifying the static Objective-J declarations to avoid the warning would
|
||||
* remove the shadowing condition from the AST, fundamentally invalidating
|
||||
* the runtime verification of the compiler's output. Until explicit block-level
|
||||
* diagnostic suppression is implemented in the parser, these specific warnings
|
||||
* must be tolerated to ensure the structural integrity of the code generator.
|
||||
*/
|
||||
|
||||
@implementation IvarTestClass : CPObject
|
||||
{
|
||||
id ivar1 @accessors;
|
||||
|
||||
Reference in new issue
Block a user