libs-gui/Source/GSAutoLayoutVFLParser.m
2022-11-22 08:21:05 +11:00

806 lines
23 KiB
Objective-C

/* Copyright (C) 2022 Free Software Foundation, Inc.
By: Benjamin Johnson
Date: 11-11-2022
This file is part of the GNUstep Library.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110 USA.
*/
#import "GSAutoLayoutVFLParser.h"
#import "GSFastEnumeration.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;
@interface GSAutoLayoutVFLParser (PrivateMethods)
- (void) parseOrientation;
- (NSArray *) parseView;
- (NSView *) parseViewName;
- (void) addFormattingConstraints:(NSView *)lastView;
- (BOOL) isVerticalEdgeFormatLayoutOption:(NSLayoutFormatOptions)options;
- (void) assertHasValidFormatLayoutOptions;
- (void) addLeadingSuperviewConstraint:(NSNumber *)spacing;
- (void) addViewSpacingConstraint:(NSNumber *)spacing
previousView:(NSView *)previousView;
- (void) addLeadingSuperviewConstraint:(NSNumber *)spacing;
- (void) addTrailingToSuperviewConstraint:(NSNumber *)spacing;
- (NSNumber *) parseLeadingSuperViewConnection;
- (NSNumber *) parseConnection;
- (NSNumber *) parseSimplePredicate;
- (NSArray *) parsePredicateList;
- (NSLayoutConstraint *) createConstraintFromParsedPredicate:
(GSObjectOfPredicate *)predicate;
- (void) parseObjectOfPredicate: (GSObjectOfPredicate *)predicate;
- (NSLayoutRelation) parseRelation;
- (NSNumber *) parsePriority;
- (NSNumber *) resolveMetricWithIdentifier:(NSString *)identifier;
- (NSView *) resolveViewWithIdentifier:(NSString *)identifier;
- (NSNumber *) parseConstant;
- (NSString *) parseIdentifier;
- (void) parseViewOpen;
- (void) parseViewClose;
- (BOOL) isValidIdentifier:(NSString *)identifer;
- (NSArray *) layoutAttributesForLayoutFormatOptions:
(NSLayoutFormatOptions)options;
- (void) failParseWithMessage:(NSString *)parseErrorMessage;
@end
@implementation GSAutoLayoutVFLParser
- (instancetype) initWithFormat:(NSString *)format
options:(NSLayoutFormatOptions)options
metrics:(NSDictionary *)metrics
views:(NSDictionary *)views
{
self = [super init];
if (self != nil)
{
_views = views;
_metrics = metrics;
_options = options;
_scanner = [NSScanner scannerWithString: format];
_constraints = [NSMutableArray array];
_layoutFormatConstraints = [NSMutableArray array];
}
return self;
}
- (NSArray *) parse
{
if ([[_scanner string] length] == 0)
{
[self failParseWithMessage: @"Cannot parse an empty string"];
}
[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: NULL])
{
[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_IN(NSNumber*, layoutAttribute, attributes)
NSLayoutConstraint *formatConstraint =
[NSLayoutConstraint constraintWithItem: lastView
attribute: [layoutAttribute integerValue]
relatedBy: NSLayoutRelationEqual
toItem: _view
attribute: [layoutAttribute integerValue]
multiplier: 1.0
constant: 0];
[_layoutFormatConstraints addObject: formatConstraint];
END_FOR_IN(attributes);
}
- (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: NULL])
{
_isVerticalOrientation = true;
}
else
{
[_scanner scanString: @"H:" intoString: NULL];
}
}
- (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 isValidIdentifier: 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: NULL];
if (!foundSuperview)
{
return nil;
}
_createLeadingConstraintToSuperview = YES;
return [self parseConnection];
}
- (NSNumber *) parseConnection
{
BOOL foundConnection = [_scanner scanString: @"-" intoString: NULL];
if (!foundConnection)
{
return [NSNumber numberWithDouble: 0];
}
NSNumber *simplePredicateValue = [self parseSimplePredicate];
BOOL endConnectionFound = [_scanner scanString: @"-" intoString: NULL];
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 isValidIdentifier: 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];
}
}
- (NSArray *) parsePredicateList
{
BOOL startsWithPredicateList = [_scanner scanString: @"(" intoString: NULL];
if (!startsWithPredicateList)
{
return [NSArray array];
}
NSMutableArray *viewPredicateConstraints = [NSMutableArray array];
BOOL shouldParsePredicate = YES;
while (shouldParsePredicate)
{
GSObjectOfPredicate predicate;
[self parseObjectOfPredicate: &predicate];
[viewPredicateConstraints
addObject: [self createConstraintFromParsedPredicate: &predicate]];
shouldParsePredicate = [_scanner scanString: @"," intoString: NULL];
}
if (![_scanner scanString: @")" intoString: NULL])
{
[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 setPriority: [predicate->priority doubleValue]];
}
return constraint;
}
- (void) parseObjectOfPredicate: (GSObjectOfPredicate *)predicate
{
NSLayoutRelation relation = [self parseRelation];
CGFloat parsedConstant;
NSView *predicatedView = nil;
BOOL scanConstantResult = [_scanner scanDouble: &parsedConstant];
if (!scanConstantResult)
{
NSString *identiferName = [self parseIdentifier];
if (![self isValidIdentifier: 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];
predicate->priority = priorityValue;
predicate->relation = relation;
predicate->constant = parsedConstant;
predicate->view = predicatedView;
}
- (NSLayoutRelation) parseRelation
{
if ([_scanner scanString: @"==" intoString: NULL])
{
return NSLayoutRelationEqual;
}
else if ([_scanner scanString: @">=" intoString: NULL])
{
return NSLayoutRelationGreaterThanOrEqual;
}
else if ([_scanner scanString: @"<=" intoString: NULL])
{
return NSLayoutRelationLessThanOrEqual;
}
else
{
return NSLayoutRelationEqual;
}
}
- (NSNumber *) parsePriority
{
NSCharacterSet *priorityMarkerCharacterSet =
[NSCharacterSet characterSetWithCharactersInString: @"@"];
BOOL foundPriorityMarker =
[_scanner scanCharactersFromSet: priorityMarkerCharacterSet
intoString: NULL];
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 isValidIdentifier: 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: NULL];
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: NULL];
if (!scannedCloseBracket)
{
[[NSException exceptionWithName: NSInternalInconsistencyException
reason: @"A view must end with a ']'"
userInfo: nil] raise];
}
}
- (BOOL) isValidIdentifier:(NSString *)identifer
{
NSRegularExpression *cIdentifierRegex = [NSRegularExpression
regularExpressionWithPattern: @"^[a-zA-Z_][a-zA-Z0-9_]*$"
options: 0
error: NULL];
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];
}
@end