Implement constraintsWithVisualFormat method on NSLayoutConstraint

This commit is contained in:
Benjamin Johnson 2022-11-05 15:45:06 +11:00
parent 30d0dc66ee
commit 1aff56cb9b
5 changed files with 569 additions and 5 deletions

View file

@ -157,7 +157,12 @@ APPKIT_EXPORT_CLASS
- (NSLayoutAnchor *) secondAnchor;
- (NSLayoutPriority) priority;
#if GS_HAS_DECLARED_PROPERTIES
@property NSLayoutPriority priority;
#else
- (NSLayoutPriority) priority;
- (void) setPriority: (NSLayoutPriority)priority;
#endif
@end

View file

@ -349,7 +349,8 @@ GSXibLoader.m \
GSXibLoading.m \
GSXibKeyedUnarchiver.m \
GSXib5KeyedUnarchiver.m \
GSHelpAttachment.m
GSHelpAttachment.m \
GSAutoLayoutVFLParser.m
# Turn off NSMenuItem warning that NSMenuItem conforms to <NSObject>,
# but does not implement <NSObject>'s methods itself (it inherits
@ -658,7 +659,8 @@ GSWindowDecorationView.h \
GSXibElement.h \
GSXibLoading.h \
GSXibKeyedUnarchiver.h \
GSHelpAttachment.h
GSHelpAttachment.h \
GSAutoLayoutVFLParser.h
libgnustep-gui_HEADER_FILES = ${GUI_HEADERS}

View file

@ -0,0 +1,22 @@
#import <AppKit/AppKit.h>
@interface GSAutoLayoutVFLParser : NSObject
{
NSDictionary *_views;
NSLayoutFormatOptions _options;
NSDictionary *_metrics;
NSScanner *_scanner;
NSMutableArray *_constraints;
NSMutableArray *_layoutFormatConstraints;
NSView *_view;
BOOL _createLeadingConstraintToSuperview;
BOOL _isVerticalOrientation;
}
-(instancetype)initWithFormat: (NSString*)format options: (NSLayoutFormatOptions)options metrics: (NSDictionary*)metrics views: (NSDictionary*)views;
-(NSArray*)parse;
@end

View file

@ -0,0 +1,526 @@
#import "GSAutoLayoutVFLParser.h"
#import <AppKit/AppKit.h>
struct GSObjectOfPredicate {
NSNumber *priority;
NSView *view;
NSLayoutRelation relation;
CGFloat constant;
};
typedef struct GSObjectOfPredicate GSObjectOfPredicate;
NSInteger const GS_DEFAULT_VIEW_SPACING = 8;
NSInteger const GS_DEFAULT_SUPERVIEW_SPACING = 20;
@implementation GSAutoLayoutVFLParser
-(instancetype)initWithFormat: (NSString*)format options: (NSLayoutFormatOptions)options metrics: (NSDictionary*)metrics views: (NSDictionary*)views
{
if (self = [super init]) {
if ([format length] == 0) {
[self failParseWithMessage:@"Cannot parse an empty string"];
}
_views = views;
_metrics = metrics;
_options = options;
_scanner = [NSScanner scannerWithString:format];
_constraints = [NSMutableArray array];
_layoutFormatConstraints = [NSMutableArray array];
}
return self;
}
-(NSArray*)parse
{
[self parseOrientation];
NSNumber *spacingConstant = [self parseLeadingSuperViewConnection];
NSView *previousView = nil;
while (![_scanner isAtEnd]) {
NSArray *viewConstraints = [self parseView];
if (_createLeadingConstraintToSuperview) {
[self addLeadingSuperviewConstraint: spacingConstant];
_createLeadingConstraintToSuperview = NO;
}
if (previousView != nil) {
[self addViewSpacingConstraint:spacingConstant previousView:previousView];
[self addFormattingConstraints: previousView];
}
[_constraints addObjectsFromArray:viewConstraints];
spacingConstant = [self parseConnection];
if ([_scanner scanString:@"|" intoString:nil]) {
[self addTrailingToSuperviewConstraint: spacingConstant];
}
previousView = _view;
}
[_constraints addObjectsFromArray:_layoutFormatConstraints];
return _constraints;
}
-(void)addFormattingConstraints: (NSView*)lastView
{
BOOL hasFormatOptions = (_options & NSLayoutFormatAlignmentMask) > 0;
if (!hasFormatOptions) {
return;
}
[self assertHasValidFormatLayoutOptions];
NSArray *attributes = [self layoutAttributesForLayoutFormatOptions:_options];
for (NSNumber *layoutAttribute in attributes) {
NSLayoutConstraint *formatConstraint = [NSLayoutConstraint constraintWithItem:lastView attribute:[layoutAttribute integerValue] relatedBy:NSLayoutRelationEqual toItem:_view attribute:[layoutAttribute integerValue] multiplier:1.0 constant:0];
[_layoutFormatConstraints addObject:formatConstraint];
}
}
-(void)assertHasValidFormatLayoutOptions
{
if (_isVerticalOrientation && [self isVerticalEdgeFormatLayoutOption: _options]) {
[self failParseWithMessage:@"A vertical alignment format option cannot be used with a vertical layout"];
} else if (!_isVerticalOrientation && ![self isVerticalEdgeFormatLayoutOption:_options]) {
[self failParseWithMessage:@"A horizontal alignment format option cannot be used with a horizontal layout"];
}
}
-(void)parseOrientation
{
if ([_scanner scanString:@"V:" intoString:nil]) {
_isVerticalOrientation = true;
} else {
[_scanner scanString:@"H:" intoString:nil];
}
}
-(NSArray*)parseView
{
[self parseViewOpen];
_view = [self parseViewName];
NSArray *viewConstraints = [self parsePredicateList];
[self parseViewClose];
return viewConstraints;
}
-(NSView*)parseViewName
{
NSString *viewName = nil;
NSCharacterSet *viewTerminators = [NSCharacterSet characterSetWithCharactersInString:@"]("];
[_scanner scanUpToCharactersFromSet:viewTerminators intoString:&viewName];
if (viewName == nil) {
[self failParseWithMessage:@"Failed to parse view name"];
}
if (![self isValidIdentifer:viewName]) {
[self failParseWithMessage:@"Invalid view name. A view name must be a valid C identifier and may only contain letters, numbers and underscores"];
}
return [self resolveViewWithIdentifier:viewName];
}
-(BOOL)isVerticalEdgeFormatLayoutOption: (NSLayoutFormatOptions)options
{
if (options & NSLayoutFormatAlignAllTop) {
return YES;
}
if (options & NSLayoutFormatAlignAllBaseline) {
return YES;
}
if (options & NSLayoutFormatAlignAllFirstBaseline) {
return YES;
}
if (options & NSLayoutFormatAlignAllBottom) {
return YES;
}
if (options & NSLayoutFormatAlignAllCenterY) {
return YES;
}
return NO;
}
-(void)addViewSpacingConstraint: (NSNumber*)spacing previousView: (NSView*)previousView
{
CGFloat viewSpacingConstant = spacing ? [spacing doubleValue] : GS_DEFAULT_VIEW_SPACING;
NSLayoutAttribute firstAttribute;
NSLayoutAttribute secondAttribute;
NSView *firstItem;
NSView *secondItem;
NSLayoutFormatOptions directionOptions = _options & NSLayoutFormatDirectionMask;
if (_isVerticalOrientation) {
firstAttribute = NSLayoutAttributeTop;
secondAttribute = NSLayoutAttributeBottom;
firstItem = _view;
secondItem = previousView;
} else if (directionOptions & NSLayoutFormatDirectionRightToLeft) {
firstAttribute = NSLayoutAttributeLeft;
secondAttribute = NSLayoutAttributeRight;
firstItem = previousView;
secondItem = _view;
} else if (directionOptions & NSLayoutFormatDirectionLeftToRight) {
firstAttribute = NSLayoutAttributeLeft;
secondAttribute = NSLayoutAttributeRight;
firstItem = _view;
secondItem = previousView;
} else {
firstAttribute = NSLayoutAttributeLeading;
secondAttribute = NSLayoutAttributeTrailing;
firstItem = _view;
secondItem = previousView;
}
NSLayoutConstraint *viewSeparatorConstraint = [NSLayoutConstraint constraintWithItem:firstItem attribute:firstAttribute relatedBy:NSLayoutRelationEqual toItem:secondItem attribute:secondAttribute multiplier:1.0 constant:viewSpacingConstant];
[_constraints addObject:viewSeparatorConstraint];
}
-(void)addLeadingSuperviewConstraint: (NSNumber*)spacing
{
NSLayoutAttribute firstAttribute;
NSView *firstItem;
NSView *secondItem;
NSLayoutFormatOptions directionOptions = _options & NSLayoutFormatDirectionMask;
if (_isVerticalOrientation) {
firstAttribute = NSLayoutAttributeTop;
firstItem = _view;
secondItem = _view.superview;
} else if (directionOptions & NSLayoutFormatDirectionRightToLeft) {
firstAttribute = NSLayoutAttributeRight;
firstItem = _view.superview;
secondItem = _view;
} else if (directionOptions & NSLayoutFormatDirectionLeftToRight) {
firstAttribute = NSLayoutAttributeLeft;
firstItem = _view;
secondItem = _view.superview;
} else {
firstAttribute = _isVerticalOrientation ? NSLayoutAttributeTop : NSLayoutAttributeLeading;
firstItem = _view;
secondItem = _view.superview;
}
CGFloat viewSpacingConstant = spacing ? [spacing doubleValue] : GS_DEFAULT_SUPERVIEW_SPACING;
NSLayoutConstraint *leadingConstraintToSuperview = [NSLayoutConstraint constraintWithItem:firstItem attribute:firstAttribute relatedBy:NSLayoutRelationEqual toItem:secondItem attribute:firstAttribute multiplier:1.0 constant:viewSpacingConstant];
[_constraints addObject:leadingConstraintToSuperview];
}
-(void)addTrailingToSuperviewConstraint: (NSNumber*)spacing
{
CGFloat viewSpacingConstant = spacing ? [spacing doubleValue] : GS_DEFAULT_SUPERVIEW_SPACING;
NSLayoutFormatOptions directionOptions = _options & NSLayoutFormatDirectionMask;
NSLayoutAttribute attribute;
NSView *firstItem;
NSView *secondItem;
if (_isVerticalOrientation) {
attribute = NSLayoutAttributeBottom;
firstItem = _view.superview;
secondItem = _view;
} else if (directionOptions & NSLayoutFormatDirectionRightToLeft) {
attribute = NSLayoutAttributeLeft;
firstItem = _view;
secondItem = _view.superview;
} else if (directionOptions & NSLayoutFormatDirectionLeftToRight) {
attribute = NSLayoutAttributeRight;
firstItem = _view.superview;
secondItem = _view;
} else {
attribute = NSLayoutAttributeTrailing;
firstItem = _view.superview;
secondItem = _view;
}
NSLayoutConstraint *trailingConstraintToSuperview = [NSLayoutConstraint constraintWithItem: firstItem attribute:attribute relatedBy:NSLayoutRelationEqual toItem: secondItem attribute:attribute multiplier:1.0 constant:viewSpacingConstant];
[_constraints addObject:trailingConstraintToSuperview];
}
-(NSNumber*)parseLeadingSuperViewConnection
{
BOOL foundSuperview = [_scanner scanString:@"|" intoString:nil];
if (!foundSuperview) {
return nil;
}
_createLeadingConstraintToSuperview = YES;
return [self parseConnection];
}
-(NSNumber*)parseConnection
{
BOOL foundConnection = [_scanner scanString:@"-" intoString:nil];
if (!foundConnection) {
return [NSNumber numberWithDouble:0];
}
NSNumber *simplePredicateValue = [self parseSimplePredicate];
BOOL endConnectionFound = [_scanner scanString:@"-" intoString:nil];
if (simplePredicateValue != nil && !endConnectionFound) {
[self failParseWithMessage:@"A connection must end with a '-'"];
} else if (simplePredicateValue == nil && endConnectionFound) {
[self failParseWithMessage:@"Found invalid connection"];
}
return simplePredicateValue;
}
-(NSNumber*)parseSimplePredicate
{
float constant;
BOOL scanConstantResult = [_scanner scanFloat:&constant];
if (scanConstantResult) {
return [NSNumber numberWithDouble:constant];
} else {
NSString *metricName = nil;
NSCharacterSet *simplePredicateTerminatorsCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"-[|"];
BOOL didParseMetricName = [_scanner scanUpToCharactersFromSet:simplePredicateTerminatorsCharacterSet intoString:&metricName];
if (!didParseMetricName) {
return nil;
}
if (![self isValidIdentifer:metricName]) {
[self failParseWithMessage:@"Invalid metric identifier. Metric identifiers must be a valid C identifier and may only contain letters, numbers and underscores"];
}
NSNumber *metric = [self resolveMetricWithIdentifier:metricName];
return metric;
}
}
-(NSArray*)parsePredicateList
{
BOOL startsWithPredicateList = [_scanner scanString:@"(" intoString:nil];
if (!startsWithPredicateList) {
return [NSArray array];
}
NSMutableArray *viewPredicateConstraints = [NSMutableArray array];
BOOL shouldParsePredicate = YES;
while (shouldParsePredicate) {
GSObjectOfPredicate *predicate = [self parseObjectOfPredicate];
[viewPredicateConstraints addObject:[self createConstraintFromParsedPredicate:predicate]];
[self freeObjectOfPredicate:predicate];
shouldParsePredicate = [_scanner scanString:@"," intoString:nil];
}
if (![_scanner scanString:@")" intoString:nil]) {
[self failParseWithMessage:@"A predicate on a view must end with ')'"];
}
return viewPredicateConstraints;
}
-(NSLayoutConstraint*)createConstraintFromParsedPredicate: (GSObjectOfPredicate*)predicate
{
NSLayoutConstraint *constraint = nil;
NSLayoutAttribute attribute = _isVerticalOrientation ? NSLayoutAttributeHeight : NSLayoutAttributeWidth;
if (predicate->view != nil) {
constraint = [NSLayoutConstraint constraintWithItem:_view attribute:attribute relatedBy:predicate->relation toItem:predicate->view attribute:attribute multiplier:1.0 constant:predicate->constant];
} else {
constraint = [NSLayoutConstraint constraintWithItem:_view attribute:attribute relatedBy:predicate->relation toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:predicate->constant];
}
if (predicate->priority) {
constraint.priority = [predicate->priority doubleValue];
}
return constraint;
}
-(GSObjectOfPredicate*)parseObjectOfPredicate
{
NSLayoutRelation relation = [self parseRelation];
CGFloat parsedConstant;
NSView *predicatedView = nil;
BOOL scanConstantResult = [_scanner scanDouble:&parsedConstant];
if (!scanConstantResult) {
NSString *identiferName = [self parseIdentifier];
if (![self isValidIdentifer:identiferName]) {
[self failParseWithMessage:@"Invalid metric or view identifier. Metric/View identifiers must be a valid C identifier and may only contain letters, numbers and underscores"];
}
NSNumber *metric = [_metrics objectForKey:identiferName];
if (metric != nil) {
parsedConstant = [metric doubleValue];
} else if ([_views objectForKey:identiferName]) {
parsedConstant = 0;
predicatedView = [_views objectForKey:identiferName];
} else {
NSString *message = [NSString stringWithFormat:@"Failed to find constant or metric for identifier '%@'", identiferName];
[self failParseWithMessage:message];
}
}
NSNumber *priorityValue = [self parsePriority];
GSObjectOfPredicate *predicate = calloc(1, sizeof(GSObjectOfPredicate));
predicate->priority = priorityValue;
predicate->relation = relation;
predicate->constant = parsedConstant;
predicate->view = predicatedView;
return predicate;
}
-(NSLayoutRelation)parseRelation
{
if ([_scanner scanString:@"==" intoString:nil]) {
return NSLayoutRelationEqual;
} else if ([_scanner scanString:@">=" intoString:nil]) {
return NSLayoutRelationGreaterThanOrEqual;
} else if ([_scanner scanString:@"<=" intoString:nil]) {
return NSLayoutRelationLessThanOrEqual;
} else {
return NSLayoutRelationEqual;
}
}
-(NSNumber*)parsePriority
{
NSCharacterSet *priorityMarkerCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"@"];
BOOL foundPriorityMarker = [_scanner scanCharactersFromSet:priorityMarkerCharacterSet intoString:nil];
if (!foundPriorityMarker) {
return nil;
}
return [self parseConstant];
}
-(NSNumber*)resolveMetricWithIdentifier: (NSString*)identifier
{
NSNumber *metric = [_metrics objectForKey:identifier];
if (metric == nil) {
[self failParseWithMessage:@"Found metric not inside metric dictionary"];
}
return metric;
}
-(NSView*)resolveViewWithIdentifier: (NSString*)identifier
{
NSView *view = [_views objectForKey:identifier];
if (view == nil) {
[self failParseWithMessage:@"Found view not inside view dictionary"];
}
return view;
}
-(NSNumber*)parseConstant
{
CGFloat constant;
BOOL scanConstantResult = [_scanner scanDouble:&constant];
if (scanConstantResult) {
return [NSNumber numberWithFloat:constant];
}
NSString *metricName = [self parseIdentifier];
if (![self isValidIdentifer:metricName]) {
[self failParseWithMessage:@"Invalid metric identifier. Metric identifiers must be a valid C identifier and may only contain letters, numbers and underscores"];
}
return [self resolveMetricWithIdentifier:metricName];
}
-(NSString*)parseIdentifier
{
NSString *identifierName = nil;
NSCharacterSet *identifierTerminators = [NSCharacterSet characterSetWithCharactersInString:@"),"];
BOOL scannedIdentifier = [_scanner scanUpToCharactersFromSet:identifierTerminators intoString:&identifierName];
if (!scannedIdentifier) {
[self failParseWithMessage:@"Failed to find constant or metric"];
}
return identifierName;
}
-(void)parseViewOpen
{
NSCharacterSet *openViewIdentifier = [NSCharacterSet characterSetWithCharactersInString:@"["];
BOOL scannedOpenBracket = [_scanner scanCharactersFromSet:openViewIdentifier intoString:nil];
if (!scannedOpenBracket) {
[[NSException exceptionWithName:NSInternalInconsistencyException reason:@"A view must start with a '['" userInfo:nil] raise];
}
}
-(void)parseViewClose
{
NSCharacterSet *closeViewIdentifier = [NSCharacterSet characterSetWithCharactersInString:@"]"];
BOOL scannedCloseBracket = [_scanner scanCharactersFromSet:closeViewIdentifier intoString:nil];
if (!scannedCloseBracket) {
[[NSException exceptionWithName:NSInternalInconsistencyException reason:@"A view must end with a ']'" userInfo:nil] raise];
}
}
-(BOOL)isValidIdentifer: (NSString*)identifer
{
NSRegularExpression *cIdentifierRegex = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z_][a-zA-Z0-9_]*$" options:0 error:nil];
NSArray *matches = [cIdentifierRegex matchesInString:identifer options:0 range:NSMakeRange(0, identifer.length)];
return [matches count] > 0;
}
-(NSArray*)layoutAttributesForLayoutFormatOptions: (NSLayoutFormatOptions)options {
NSMutableArray *attributes = [NSMutableArray array];
if (options & NSLayoutFormatAlignAllLeft) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeLeft]];
}
if (options & NSLayoutFormatAlignAllRight) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeRight]];
}
if (options & NSLayoutFormatAlignAllTop) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeTop]];
}
if (options & NSLayoutFormatAlignAllBottom) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeBottom]];
}
if (options & NSLayoutFormatAlignAllLeading) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeLeading]];
}
if (options & NSLayoutFormatAlignAllTrailing) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeTrailing]];
}
if (options & NSLayoutFormatAlignAllCenterX) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeCenterX]];
}
if (options & NSLayoutFormatAlignAllCenterY) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeCenterY]];
}
if (options & NSLayoutFormatAlignAllBaseline) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeBaseline]];
}
if (options & NSLayoutFormatAlignAllFirstBaseline) {
[attributes addObject:[NSNumber numberWithInteger:NSLayoutAttributeFirstBaseline]];
}
if ([attributes count] == 0) {
[self failParseWithMessage:@"Unrecognized layout formatting option"];
}
return attributes;
}
-(void)failParseWithMessage: (NSString*)parseErrorMessage
{
NSException *parseException = [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"Unable to parse constraint format: %@", parseErrorMessage] userInfo:nil];
[parseException raise];
}
-(void)freeObjectOfPredicate: (GSObjectOfPredicate*)predicate
{
predicate->view = nil;
predicate->priority = nil;
free(predicate);
}
@end

View file

@ -32,6 +32,7 @@
#import "AppKit/NSLayoutConstraint.h"
#import "AppKit/NSWindow.h"
#import "AppKit/NSApplication.h"
#import "GSAutoLayoutVFLParser.h"
static NSMutableArray *activeConstraints = nil;
// static NSNotificationCenter *nc = nil;
@ -236,8 +237,16 @@ static NSMutableArray *activeConstraints = nil;
metrics: (NSDictionary *)metrics
views: (NSDictionary *)views
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity: 10];
return array;
GSAutoLayoutVFLParser *parser = [[GSAutoLayoutVFLParser alloc]
initWithFormat: fmt
options: opt
metrics: metrics
views: views];
NSArray *constraints = [parser parse];
[parser release];
return constraints;
}
- (instancetype) initWithItem: (id)firstItem