diff --git a/AppKit/AppKit.j b/AppKit/AppKit.j index edf1dd794..4cd54116c 100644 --- a/AppKit/AppKit.j +++ b/AppKit/AppKit.j @@ -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" diff --git a/AppKit/CPStackView.j b/AppKit/CPStackView.j index c05d776b9..856f9fbaf 100644 --- a/AppKit/CPStackView.j +++ b/AppKit/CPStackView.j @@ -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 + +// 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"]; diff --git a/Tests/Manual/CPStackViewTest/Jakefile b/Tests/Manual/CPStackViewTest/Jakefile index a4174055a..65c8df6cf 100644 --- a/Tests/Manual/CPStackViewTest/Jakefile +++ b/Tests/Manual/CPStackViewTest/Jakefile @@ -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; + } }