diff --git a/docs/wayland-port.md b/docs/wayland-port.md index 834c260fc..3d80b3f3b 100644 --- a/docs/wayland-port.md +++ b/docs/wayland-port.md @@ -14580,3 +14580,38 @@ would immediately call `CurrentValueSubject.init`, `AnyCancellable.store(in:)` a Implementing them means writing Combine's core semantics against the Swift calling convention, which is a project rather than a rung. What has been proved is the method and three of its steps: a generic struct, a class, and the exact point where a generic class stops. + +## Auto Layout exists now, and the version number still does not move (2026-08-21) + +Task #150 ended with a one-line change that could not land: raising `NSAppKitVersionNumber` past +10.15 buys MoneyMoney's truncated menu titles seven points of room, and kills the application on +`+[NSLayoutConstraint constraintWithItem:...]`, because cocotron's `NSLayoutConstraint.m` was +twenty-three lines with no class in it. + +**There is a class now**, and the first attempt at it was in the wrong framework. Implemented in +AppKit, the selector stayed unrecognized: **this Foundation has carried an `NSLayoutConstraint` stub +since the nib decoder needed something to decode into**, along with three subclasses of it, and two +Objective-C classes with one name is a coin toss the runtime announces and then resolves however it +likes. It resolved to the stub, whose `methodSignatureForSelector:` answers `"v@:"` for everything. +So the object lives in Foundation, where the name already was, and AppKit imports that header. + +It is the constraint and its accounting, and **no solver**: the properties, the factory, activate and +deactivate, and `-setActive:`, which puts the constraint on a view and takes it off again (asked by +selector, because `NSView` is AppKit's and this is Foundation). Every view in these nibs still carries +an autoresizing mask and `translatesAutoresizingMaskIntoConstraints` defaults to YES, so the layout is +exactly what it was. + +With that and `+[NSScreen screensHaveSeparateSpaces]` (which MoneyMoney asks inside +`-[NSWindow setFrameAutosaveName:]`, early enough that an unrecognized selector there leaves the +window half built and the application catches the exception, so nothing crashes and nothing draws), +the modern version number runs clean: **zero unrecognized selectors, zero uncaught exceptions**, and +the menu titles gain their two characters: + + 1504: Send batc… New tran… New inst… New forei… Use as tr… Change t… + 2022: Send batch t… New transf… New instan… New foreig… Use as tran… Change tra… + +**And it still does not land, because of the capture rather than the log.** At 2022 MoneyMoney draws +**no menu bar at all** and an **empty toolbar**: it takes the Big Sur chrome path, silently, and this +AppKit does not implement it. Two characters of menu title against the menu bar is not a trade worth +making, so the constant went back to 1504 with that written where it lives. The class stays: it is +what makes the next attempt possible, and it is the right shape regardless. diff --git a/vendor/patches/foundation/0030-a-real-nslayoutconstraint-where-the-stub-was.patch b/vendor/patches/foundation/0030-a-real-nslayoutconstraint-where-the-stub-was.patch new file mode 100644 index 000000000..a4ad020af --- /dev/null +++ b/vendor/patches/foundation/0030-a-real-nslayoutconstraint-where-the-stub-was.patch @@ -0,0 +1,250 @@ +A real NSLayoutConstraint, in the framework that already had a stub of it. + +MONEYMONEY DIES ON +constraintWithItem: THE MOMENT APPKIT CLAIMS TO BE NEWER THAN 10.15, which is the +band that also changes its menu metric, so the two are tied together: the cosmetic fix in task #150 +cannot land while the class is a stub. + +The class belongs to AppKit on macOS and that is where the implementation was written first. It could +not stay there. THIS Foundation has carried an NSLayoutConstraint since the nib decoder needed +something to decode into, along with three subclasses of it, and two Objective-C classes with one +name is a coin toss the runtime announces and then resolves however it likes. It resolved to the +stub, whose methodSignatureForSelector answers "v@:" for everything, and +constraintWithItem: stayed +unrecognized however carefully AppKit implemented it. So the object lives here and AppKit imports +this header. + +WHAT IT IS: the constraint, its properties, the factory, activate and deactivate, and -setActive:, +which puts the constraint on a view and takes it off again. Asked BY SELECTOR, because NSView is +AppKit's and this is Foundation. + +WHAT IT IS NOT: a solver. Nothing here lays anything out. Every view in these nibs still carries an +autoresizing mask and translatesAutoresizingMaskIntoConstraints defaults to YES, so the layout is +exactly what it was. An application that genuinely lays out BY constraint gets a window whose +subviews sit where the mask puts them, which is wrong but visible, rather than an unrecognized +selector, which stops it dead. + +MEASURED: with this and +[NSScreen screensHaveSeparateSpaces], MoneyMoney runs at +NSAppKitVersionNumber 2022 with zero unrecognized selectors and zero uncaught exceptions, and its +truncated menu titles gain the two characters the 35 point metric buys. The version number is still +1504 and the reason is in the capture, not the log: at 2022 the application draws no menu bar at all +and an empty toolbar, because it takes the Big Sur chrome path. See AppKit/NSApplication.m. +--- a/include/Foundation/NSLayoutConstraint.h ++++ b/include/Foundation/NSLayoutConstraint.h +@@ -1,5 +1,89 @@ + #import ++#import ++#import + +-@interface NSLayoutConstraint : NSObject ++/* ++ * THE CLASS LIVES HERE BECAUSE IT ALREADY DID, and two of them is one too many. ++ * ++ * On macOS NSLayoutConstraint is AppKit's, and that is where the implementation was written first. ++ * It could not stay there: this Foundation has carried a stub of the same name since the nib ++ * decoder needed something to decode into, along with three subclasses of it, and a duplicate ++ * Objective-C class is a coin toss the runtime announces and then resolves however it likes. The ++ * application got the stub, whose methodSignatureForSelector answers "v@:" for everything, and ++ * +constraintWithItem: was unrecognized however carefully AppKit implemented it. ++ * ++ * So the object and its accounting live here, and AppKit imports this header. Nothing solves the ++ * constraints; see the note in AppKit/NSLayoutConstraint.h for why that is the right amount of Auto ++ * Layout for the applications in front of us. ++ */ ++ ++typedef float NSLayoutPriorityFoundation; ++ ++typedef NS_ENUM(NSInteger, NSLayoutRelation) { ++ NSLayoutRelationLessThanOrEqual = -1, ++ NSLayoutRelationEqual = 0, ++ NSLayoutRelationGreaterThanOrEqual = 1, ++}; ++ ++typedef NS_ENUM(NSInteger, NSLayoutAttribute) { ++ NSLayoutAttributeNotAnAttribute = 0, ++ NSLayoutAttributeLeft = 1, ++ NSLayoutAttributeRight, ++ NSLayoutAttributeTop, ++ NSLayoutAttributeBottom, ++ NSLayoutAttributeLeading, ++ NSLayoutAttributeTrailing, ++ NSLayoutAttributeWidth, ++ NSLayoutAttributeHeight, ++ NSLayoutAttributeCenterX, ++ NSLayoutAttributeCenterY, ++ NSLayoutAttributeLastBaseline, ++ NSLayoutAttributeFirstBaseline, ++}; ++ ++enum { ++ NSLayoutPriorityRequired = 1000, ++ NSLayoutPriorityDefaultHigh = 750, ++ NSLayoutPriorityDragThatCanResizeWindow = 510, ++ NSLayoutPriorityWindowSizeStayPut = 500, ++ NSLayoutPriorityDragThatCannotResizeWindow = 490, ++ NSLayoutPriorityDefaultLow = 250, ++ NSLayoutPriorityFittingSizeCompression = 50, ++}; ++ ++@interface NSLayoutConstraint : NSObject { ++ id _firstItem; ++ id _secondItem; ++ NSLayoutAttribute _firstAttribute; ++ NSLayoutAttribute _secondAttribute; ++ NSLayoutRelation _relation; ++ double _multiplier; ++ double _constant; ++ float _priority; ++ BOOL _active; ++ NSString *_identifier; ++} ++ +++ (instancetype) constraintWithItem: (id) firstItem ++ attribute: (NSLayoutAttribute) firstAttribute ++ relatedBy: (NSLayoutRelation) relation ++ toItem: (id) secondItem ++ attribute: (NSLayoutAttribute) secondAttribute ++ multiplier: (double) multiplier ++ constant: (double) constant; ++ +++ (void) activateConstraints: (NSArray *) constraints; +++ (void) deactivateConstraints: (NSArray *) constraints; ++ ++@property (readonly, assign) id firstItem; ++@property (readonly, assign) id secondItem; ++@property (readonly) NSLayoutAttribute firstAttribute; ++@property (readonly) NSLayoutAttribute secondAttribute; ++@property (readonly) NSLayoutRelation relation; ++@property (readonly) double multiplier; ++@property double constant; ++@property float priority; ++@property (getter=isActive) BOOL active; ++@property (copy) NSString *identifier; + + @end +--- a/src/NSLayoutConstraint.m ++++ b/src/NSLayoutConstraint.m +@@ -21,21 +21,118 @@ + #import + #import + +-#warning TODO: $ld$hide$os 10.4 through 10.7, also METACLASS + + @implementation NSLayoutConstraint + +-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { +- return [NSMethodSignature signatureWithObjCTypes: "v@:"]; ++@synthesize firstItem = _firstItem; ++@synthesize secondItem = _secondItem; ++@synthesize firstAttribute = _firstAttribute; ++@synthesize secondAttribute = _secondAttribute; ++@synthesize relation = _relation; ++@synthesize multiplier = _multiplier; ++@synthesize constant = _constant; ++@synthesize priority = _priority; ++@synthesize identifier = _identifier; ++ +++ (instancetype) constraintWithItem: (id) firstItem ++ attribute: (NSLayoutAttribute) firstAttribute ++ relatedBy: (NSLayoutRelation) relation ++ toItem: (id) secondItem ++ attribute: (NSLayoutAttribute) secondAttribute ++ multiplier: (double) multiplier ++ constant: (double) constant ++{ ++ NSLayoutConstraint *result = [[[self alloc] init] autorelease]; ++ ++ result->_firstItem = firstItem; ++ result->_secondItem = secondItem; ++ result->_firstAttribute = firstAttribute; ++ result->_secondAttribute = secondAttribute; ++ result->_relation = relation; ++ result->_multiplier = multiplier; ++ result->_constant = constant; ++ result->_priority = NSLayoutPriorityRequired; ++ return result; + } + +-- (id)initWithCoder:(NSCoder *)coder { +- NSLog(@"Stub called: initWithCoder in NSLayoutConstraint"); +++ (void) activateConstraints: (NSArray *) constraints ++{ ++ for (NSLayoutConstraint *constraint in constraints) { ++ [constraint setActive: YES]; ++ } ++} ++ +++ (void) deactivateConstraints: (NSArray *) constraints ++{ ++ for (NSLayoutConstraint *constraint in constraints) { ++ [constraint setActive: NO]; ++ } ++} ++ ++- (id) initWithCoder: (NSCoder *) coder ++{ ++ self = [super init]; ++ if (self != nil) { ++ _priority = NSLayoutPriorityRequired; ++ } + return self; + } + +-- (void)forwardInvocation:(NSInvocation *)anInvocation { +- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]); ++- (void) dealloc ++{ ++ [_identifier release]; ++ [super dealloc]; ++} ++ ++- (BOOL) isActive ++{ ++ return _active; ++} ++ ++/* ++ * ACTIVATING A CONSTRAINT IS WHAT PUTS IT ON A VIEW, and macOS picks the nearest common ancestor of ++ * the two items. Holding it on the first item is enough here, because nothing reads the list back ++ * except the view that owns it, and what an application can otherwise notice is the constraint being ++ * deallocated the moment the statement that made it ends. ++ * ++ * ASKED BY SELECTOR, NOT BY CLASS. NSView is AppKit's and this is Foundation; respondsToSelector is ++ * how a lower framework talks to a higher one without a dependency it cannot have. ++ */ ++- (void) setActive: (BOOL) active ++{ ++ if (active == _active) { ++ return; ++ } ++ _active = active; ++ ++ id owner = nil; ++ ++ if ([_firstItem respondsToSelector: @selector(superview)]) { ++ owner = [_firstItem performSelector: @selector(superview)]; ++ if (owner == nil) { ++ owner = _firstItem; ++ } ++ } else if ([_secondItem respondsToSelector: @selector(superview)]) { ++ owner = _secondItem; ++ } ++ if (owner == nil) { ++ return; ++ } ++ if (active) { ++ if ([owner respondsToSelector: @selector(addConstraint:)]) { ++ [owner performSelector: @selector(addConstraint:) withObject: self]; ++ } ++ } else if ([owner respondsToSelector: @selector(removeConstraint:)]) { ++ [owner performSelector: @selector(removeConstraint:) withObject: self]; ++ } ++} ++ ++- (NSString *) description ++{ ++ return [NSString stringWithFormat: @"<%@ %p first=%@ attr=%ld rel=%ld second=%@ attr=%ld x%g +%g%@>", ++ [self class], self, [_firstItem class], (long) _firstAttribute, ++ (long) _relation, [_secondItem class], (long) _secondAttribute, ++ _multiplier, _constant, _active ? @" active" : @""]; + } + + @end diff --git a/vendor/pins/cocotron/AppKit/NSApplication.m b/vendor/pins/cocotron/AppKit/NSApplication.m index 94a72d1ef..972b7bae2 100644 --- a/vendor/pins/cocotron/AppKit/NSApplication.m +++ b/vendor/pins/cocotron/AppKit/NSApplication.m @@ -99,10 +99,21 @@ NSString *const NSApplicationLaunchUserNotificationKey = @"NSApplicationLaunchUserNotificationKey"; /* - * THE VERSION APPLICATIONS BRANCH ON, and 10.12 is deliberate even though SystemVersion.plist says - * 14.4.1. Anything newer than 10.15 makes MoneyMoney build its wizard with Auto Layout, and - * NSLayoutConstraint here is two constants and no class, so the application dies on - * +constraintWithItem: before it draws. Raise this only together with Auto Layout. + * THE VERSION APPLICATIONS BRANCH ON, and 10.12 is still deliberate. What changed is WHY. + * + * It used to be pinned here because anything newer than 10.15 made MoneyMoney build its wizard with + * Auto Layout and die on +constraintWithItem: before drawing. There is a real NSLayoutConstraint + * now (in Foundation, where the class already lived), and +[NSScreen screensHaveSeparateSpaces] + * answers as well, so 2022 no longer throws: zero unrecognized selectors, zero uncaught exceptions, + * and the menu titles gain the two characters the 35 point metric buys them. + * + * IT STILL COSTS MORE THAN IT BUYS, and the capture is what says so. At 2022 MoneyMoney draws NO + * MENU BAR AT ALL and an EMPTY TOOLBAR: it takes the Big Sur chrome path, which this AppKit does not + * implement, and it does it silently rather than by raising. Two characters of menu title against + * the menu bar is not a trade worth making. + * + * So: raise this when the Big Sur toolbar and menu bar are drawn, not before, and check the capture + * rather than the exception count. */ const NSAppKitVersion NSAppKitVersionNumber = 1504; // macOS 10.12 diff --git a/vendor/pins/cocotron/AppKit/NSLayoutConstraint.m b/vendor/pins/cocotron/AppKit/NSLayoutConstraint.m index aa79e6759..7ae42031a 100644 --- a/vendor/pins/cocotron/AppKit/NSLayoutConstraint.m +++ b/vendor/pins/cocotron/AppKit/NSLayoutConstraint.m @@ -18,6 +18,9 @@ */ #import +#import +#import const CGFloat NSViewNoInstrinsicMetric = 0xbff0000000000000; const CGFloat NSViewNoIntrinsicMetric = 0xbff0000000000000; + diff --git a/vendor/pins/cocotron/AppKit/NSScreen.m b/vendor/pins/cocotron/AppKit/NSScreen.m index b1c239434..6cadca764 100644 --- a/vendor/pins/cocotron/AppKit/NSScreen.m +++ b/vendor/pins/cocotron/AppKit/NSScreen.m @@ -65,6 +65,19 @@ NSNotificationName const NSScreenColorSpaceDidChangeNotification = @"NSScreenCol return result; } +/* + * NO, AND SAYING SO IS THE WHOLE FEATURE. macOS answers this from the Mission Control preference + * that gives each display its own set of Spaces; there is one screen here and no Spaces at all. + * + * MoneyMoney asks it inside -[NSWindow setFrameAutosaveName:], which is early enough in window setup + * that an unrecognized selector there leaves the window half built. The application CATCHES the + * exception, so nothing crashes and nothing draws either, which is the shape described in the plan: + * a caught exception hides a whole feature. + */ ++ (BOOL) screensHaveSeparateSpaces { + return NO; +} + + (NSArray *) screens { return [[NSDisplay currentDisplay] screens]; } diff --git a/vendor/pins/cocotron/AppKit/NSView.m b/vendor/pins/cocotron/AppKit/NSView.m index a312c4b16..dd0c17e6d 100644 --- a/vendor/pins/cocotron/AppKit/NSView.m +++ b/vendor/pins/cocotron/AppKit/NSView.m @@ -582,6 +582,7 @@ typedef struct __VFlags { withObject: nil]; [_subviews release]; + [_constraints release]; [_draggedTypes release]; [_trackingAreas release]; [_contentFilters release]; @@ -3696,6 +3697,44 @@ static NSView *viewBeingPrinted = nil; NSUnimplementedMethod(); } + +/* + * THE CONSTRAINTS A VIEW HOLDS, and nothing that solves them. An application that only builds + * constraints and activates them (MoneyMoney makes exactly two) needs the object to exist, to be + * retained, and to be findable again. Layout is still the autoresizing path. + */ +- (NSArray *) constraints { + return _constraints ? [NSArray arrayWithArray: _constraints] : [NSArray array]; +} + +- (void) addConstraint: (NSLayoutConstraint *) constraint { + if (constraint == nil) { + return; + } + if (_constraints == nil) { + _constraints = [[NSMutableArray alloc] init]; + } + if (![_constraints containsObject: constraint]) { + [_constraints addObject: constraint]; + } +} + +- (void) addConstraints: (NSArray *) constraints { + for (NSLayoutConstraint *constraint in constraints) { + [self addConstraint: constraint]; + } +} + +- (void) removeConstraint: (NSLayoutConstraint *) constraint { + [_constraints removeObject: constraint]; +} + +- (void) removeConstraints: (NSArray *) constraints { + for (NSLayoutConstraint *constraint in constraints) { + [self removeConstraint: constraint]; + } +} + - (NSLayoutPriority) contentHuggingPriorityForOrientation: (NSLayoutConstraintOrientation) orientation { if (orientation == NSLayoutConstraintOrientationHorizontal) { return _horizontalContentHuggingPriority; diff --git a/vendor/pins/cocotron/AppKit/include/AppKit/NSLayoutConstraint.h b/vendor/pins/cocotron/AppKit/include/AppKit/NSLayoutConstraint.h index c92583eb1..2a0722da2 100644 --- a/vendor/pins/cocotron/AppKit/include/AppKit/NSLayoutConstraint.h +++ b/vendor/pins/cocotron/AppKit/include/AppKit/NSLayoutConstraint.h @@ -20,6 +20,8 @@ #import #import #import +#import +#import typedef float NSLayoutPriority; @@ -30,3 +32,18 @@ typedef NS_ENUM(NSInteger, NSLayoutConstraintOrientation) { APPKIT_EXPORT const CGFloat NSViewNoInstrinsicMetric; APPKIT_EXPORT const CGFloat NSViewNoIntrinsicMetric; + +/* + * AUTO LAYOUT, AS FAR AS AN APPLICATION THAT ONLY MAKES CONSTRAINTS NEEDS IT. + * + * MoneyMoney reaches for exactly three things once AppKit claims to be newer than 10.15: + * +constraintWithItem:..., the class itself, and -setActive:. Two constraints, activated once. What + * it does NOT do is rely on a solver for its layout, because every view in its nibs still carries an + * autoresizing mask and translatesAutoresizingMaskIntoConstraints defaults to YES. + * + * So there is an object and its accounting, and no solver. THE CLASS ITSELF IS IN FOUNDATION, which + * has carried a stub of that name since the nib decoder needed one, along with three subclasses of + * it. Two Objective-C classes with one name is a coin toss the runtime announces and then resolves + * however it likes, and the application got the stub. See Foundation/NSLayoutConstraint.h. + */ +#import diff --git a/vendor/pins/cocotron/AppKit/include/AppKit/NSScreen.h b/vendor/pins/cocotron/AppKit/include/AppKit/NSScreen.h index 260b5706e..69451a14a 100644 --- a/vendor/pins/cocotron/AppKit/include/AppKit/NSScreen.h +++ b/vendor/pins/cocotron/AppKit/include/AppKit/NSScreen.h @@ -38,6 +38,7 @@ typedef NSString *NSDeviceDescriptionKey; + (NSScreen *) mainScreen; + (NSArray *) screens; ++ (BOOL) screensHaveSeparateSpaces; - initWithFrame: (NSRect) frame visibleFrame: (NSRect) visibleFrame; diff --git a/vendor/pins/cocotron/AppKit/include/AppKit/NSView.h b/vendor/pins/cocotron/AppKit/include/AppKit/NSView.h index 50a23322c..b2f278c39 100644 --- a/vendor/pins/cocotron/AppKit/include/AppKit/NSView.h +++ b/vendor/pins/cocotron/AppKit/include/AppKit/NSView.h @@ -86,6 +86,8 @@ APPKIT_EXPORT const NSNotificationName NSViewGlobalFrameDidChangeNotification; APPKIT_EXPORT const NSViewFullScreenModeOptionKey NSFullScreenModeApplicationPresentationOptions; +@class NSLayoutConstraint; + @interface NSView : NSResponder { NSRect _frame; NSRect _bounds; @@ -112,6 +114,7 @@ APPKIT_EXPORT const NSViewFullScreenModeOptionKey NSFullScreenModeApplicationPre CGFloat _frameRotation; CGFloat _boundsRotation; BOOL _translatesAutoresizingMaskIntoConstraints; + NSMutableArray *_constraints; BOOL _validTrackingAreas; BOOL _validTransforms; @@ -144,6 +147,15 @@ APPKIT_EXPORT const NSViewFullScreenModeOptionKey NSFullScreenModeApplicationPre @property(class, readonly) BOOL requiresConstraintBasedLayout; @property BOOL translatesAutoresizingMaskIntoConstraints; +/* THE CONSTRAINTS A VIEW HOLDS. Nothing here solves them, see NSLayoutConstraint.h; a view keeps + * them so that activating one has somewhere to go and so that it outlives the statement that made + * it, which is what an application can otherwise notice. */ +@property (readonly, copy) NSArray *constraints; +- (void) addConstraint: (NSLayoutConstraint *) constraint; +- (void) addConstraints: (NSArray *) constraints; +- (void) removeConstraint: (NSLayoutConstraint *) constraint; +- (void) removeConstraints: (NSArray *) constraints; + + (NSView *) focusView; + (NSMenu *) defaultMenu; + (NSFocusRingType) defaultFocusRingType;