From 5ab10f40243e049e370bf78439bc68f7b3019158 Mon Sep 17 00:00:00 2001 From: cacaodev Date: Sun, 27 Dec 2015 15:34:37 +0100 Subject: [PATCH 01/16] NEW: CPExpression +expressionForBlock:arguments: An expression that returns the result of evaluating a block. With tests. --- Foundation/CPPredicate/CPExpression.j | 1 + Foundation/CPPredicate/_CPBlockExpression.j | 83 +++++++++++++++++++++ Foundation/CPPredicate/_CPExpression.j | 25 +++++++ Tests/Foundation/CPPredicateTest.j | 20 +++++ 4 files changed, 129 insertions(+) create mode 100644 Foundation/CPPredicate/_CPBlockExpression.j diff --git a/Foundation/CPPredicate/CPExpression.j b/Foundation/CPPredicate/CPExpression.j index 06ed6b0ba..12ad45a74 100644 --- a/Foundation/CPPredicate/CPExpression.j +++ b/Foundation/CPPredicate/CPExpression.j @@ -28,3 +28,4 @@ @import "_CPAggregateExpression.j" @import "_CPSetExpression.j" @import "_CPSubqueryExpression.j" +@import "_CPBlockExpression.j" diff --git a/Foundation/CPPredicate/_CPBlockExpression.j b/Foundation/CPPredicate/_CPBlockExpression.j new file mode 100644 index 000000000..109d5d517 --- /dev/null +++ b/Foundation/CPPredicate/_CPBlockExpression.j @@ -0,0 +1,83 @@ +/* + * _CPBlockExpression.j + * + * Created by cacaodev. + * Copyright 2015. + * + * 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-1301 USA + */ + +@import "_CPExpression.j" + +@implementation _CPBlockExpression : CPExpression +{ + Function _block; + CPArray _arguments; +} + +- (id)initWithBlock:(Function)aBlock arguments:(CPArray)arguments +{ + self = [super initWithExpressionType:CPBlockExpressionType]; + + if (self) + { + _block = aBlock; + _arguments = arguments; + } + + return self; +} + +- (BOOL)isEqual:(id)object +{ + if (self === object) + return YES; + + if (object === nil || object.isa !== self.isa || [object expressionBlock] !== _block || ![[object arguments] isEqual:_arguments]) + return NO; + + return YES; +} + +- (Function)expressionBlock +{ + return _block; +} + +- (CPArray)arguments +{ + return _arguments; +} + +- (id)expressionValueWithObject:(id)object context:(CPDictionary)context +{ + var args = []; + + if (_arguments !== nil) + { + args = _arguments.map(function(expression){ + return [expression expressionValueWithObject:object context:context]; + }); + } + + return _block(object, args, context); +} + +- (CPString)description +{ + return [CPString stringWithFormat:@"Block(0x%@)", [CPString stringWithHash:[self UID]]]; +} + +@end \ No newline at end of file diff --git a/Foundation/CPPredicate/_CPExpression.j b/Foundation/CPPredicate/_CPExpression.j index 5a9561cb7..02fb98801 100644 --- a/Foundation/CPPredicate/_CPExpression.j +++ b/Foundation/CPPredicate/_CPExpression.j @@ -65,6 +65,10 @@ CPIntersectSetExpressionType = 8; An expression that combines two nested expression results by set subtraction. */ CPMinusSetExpressionType = 9; +/*! + An expression that returns the result of evaluating a block. +*/ +CPBlockExpressionType = 10; /*! @ingroup foundation @@ -259,6 +263,27 @@ CPMinusSetExpressionType = 9; return [[_CPSubqueryExpression alloc] initWithExpression:expression usingIteratorVariable:variable predicate:predicate]; } +/*! + Returns Creates an NSExpression object that will use the Block for evaluating objects. + @param aBlock The Block is applied to the object to be evaluated. + +The Block takes three arguments and returns a value: + +evaluatedObject +The object to be evaluated. +expressions +An array of predicate expressions that evaluates to a collection. +context +A dictionary that the expression can use to store temporary state for one predicate evaluation. + +@discussion Note that context is mutable, and that it can only be accessed during the evaluation of the expression. +@param arguments An array containing NSExpression objects that will be used as parameters during the invocation of the block. +*/ ++ (CPExpression)expressionForBlock:(Function)aBlock arguments:(CPArray)args +{ + return [[_CPBlockExpression alloc] initWithBlock:aBlock arguments:args]; +} + // Getting Information About an Expression /*! Returns the expression type for the receiver. diff --git a/Tests/Foundation/CPPredicateTest.j b/Tests/Foundation/CPPredicateTest.j index f167d5513..2d6f8210a 100644 --- a/Tests/Foundation/CPPredicateTest.j +++ b/Tests/Foundation/CPPredicateTest.j @@ -79,6 +79,13 @@ var expression_minusset = [CPExpression expressionForMinusSet:set with:array]; [self assertNotNull:expression_minusset message:"MinusSet Expression should not be nil"]; + + var expression_block = [CPExpression expressionForBlock:function(obj, args, bindings) + { + return obj; + } arguments:nil]; + + [self assertNotNull:expression_block message:"Block Expression should not be nil"]; } - (void)testSetExpressionEvaluation @@ -147,6 +154,19 @@ [self assertTrue:([eval isEqual:expected]) message:"'" + [expression predicateFormat] + "' result is "+ eval + "but should be " + expected]; } +- (void)testBlockExpressionEvaluation +{ + var block = function(obj, args, bindings) + { + return [obj stringByAppendingString:[args componentsJoinedByString:"-"]]; + }; + + var expression = [CPExpression expressionForBlock:block arguments:[[CPExpression expressionForConstantValue:"A"], [CPExpression expressionForConstantValue:"B"]]]; + + var eval = [expression expressionValueWithObject:"OBJ" context:@{}]; + [self assertTrue:([eval isEqual:"OBJA-B"]) message:"'" + [expression description] + "' result is "+ eval + "but should be " + "OBJA-B"]; +} + - (void)testOptions { var pred = [[CPComparisonPredicate alloc] initWithLeftExpression:[CPExpression expressionForConstantValue:@"àa"] rightExpression:[CPExpression expressionForConstantValue:@"aà"] modifier:CPDirectPredicateModifier type:CPLikePredicateOperatorType options:3]; From 07b6e2b9f15c7e307434f98a5381f1477ea6c79c Mon Sep 17 00:00:00 2001 From: cacaodev Date: Sun, 27 Dec 2015 15:40:32 +0100 Subject: [PATCH 02/16] CPExpression +expressionForConditional:trueExpression:falseExpression: Conditional Expression : an expression using different expressions for evaluation, depending of a predicate result. Support for predicate parsing: ```TERNARY(predicate, trueExpression, falseExpression)``` With init, equal, evaluation, parsing tests. --- Foundation/CPPredicate/CPExpression.j | 1 + .../CPPredicate/_CPConditionalExpression.j | 84 +++++++++++++++++++ Foundation/CPPredicate/_CPExpression.j | 9 ++ Foundation/CPPredicate/_CPPredicate.j | 26 +++++- Tests/Foundation/CPPredicateTest.j | 23 +++++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 Foundation/CPPredicate/_CPConditionalExpression.j diff --git a/Foundation/CPPredicate/CPExpression.j b/Foundation/CPPredicate/CPExpression.j index 12ad45a74..10ca2b598 100644 --- a/Foundation/CPPredicate/CPExpression.j +++ b/Foundation/CPPredicate/CPExpression.j @@ -29,3 +29,4 @@ @import "_CPSetExpression.j" @import "_CPSubqueryExpression.j" @import "_CPBlockExpression.j" +@import "_CPConditionalExpression.j" diff --git a/Foundation/CPPredicate/_CPConditionalExpression.j b/Foundation/CPPredicate/_CPConditionalExpression.j new file mode 100644 index 000000000..6e636ec29 --- /dev/null +++ b/Foundation/CPPredicate/_CPConditionalExpression.j @@ -0,0 +1,84 @@ +/* + * _CPConditionalExpression.j + * + * Created by cacaodev. + * Copyright 2015. + * + * 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-1301 USA + */ +@import "_CPPredicate.j" +@import "_CPExpression.j" + +@implementation _CPConditionalExpression : CPExpression +{ + CPPredicate _predicate; + CPExpression _trueExpression; + CPExpression _falseExpression; +} + +- (id)initWithPredicate:(CPPredicate)aPredicate trueExpression:(CPExpression)trueExpression falseExpression:(CPExpression)falseExpression +{ + self = [super initWithExpressionType:CPConditionalExpressionType]; + + if (self) + { + _predicate = aPredicate; + _trueExpression = trueExpression; + _falseExpression = falseExpression; + } + + return self; +} + +- (BOOL)isEqual:(id)object +{ + if (self === object) + return YES; + + if (object === nil || object.isa !== self.isa || ![[object predicate] isEqual:_predicate] || ![[object trueExpression] isEqual:_trueExpression] || ![[object falseExpression] isEqual:_falseExpression]) + return NO; + + return YES; +} + +- (CPPredicate)predicate +{ + return _predicate; +} + +- (CPExpression)trueExpression +{ + return _trueExpression; +} + +- (CPExpression)falseExpression +{ + return _falseExpression; +} + +- (id)expressionValueWithObject:(id)object context:(CPDictionary)context +{ + var eval = [_predicate evaluateWithObject:object substitutionVariables:context]; + var exp = eval ? _trueExpression : _falseExpression; + + return [exp expressionValueWithObject:object context:context]; +} + +- (CPString)description +{ + return [CPString stringWithFormat:@"TERNARY(%@,%@,%@)", [_predicate predicateFormat], [_trueExpression description], [_falseExpression description]]; +} + +@end \ No newline at end of file diff --git a/Foundation/CPPredicate/_CPExpression.j b/Foundation/CPPredicate/_CPExpression.j index 02fb98801..b26707fc0 100644 --- a/Foundation/CPPredicate/_CPExpression.j +++ b/Foundation/CPPredicate/_CPExpression.j @@ -69,6 +69,10 @@ CPMinusSetExpressionType = 9; An expression that returns the result of evaluating a block. */ CPBlockExpressionType = 10; +/*! + An expression that returns an expression that depends on the evaluation of a predicate. +*/ +CPConditionalExpressionType = 11; /*! @ingroup foundation @@ -284,6 +288,11 @@ A dictionary that the expression can use to store temporary state for one predic return [[_CPBlockExpression alloc] initWithBlock:aBlock arguments:args]; } ++ (CPExpression)expressionForConditional:(CPPredicate)aPredicate trueExpression:(CPExpression)trueExpression falseExpression:(CPExpression)falseExpression +{ + return [[_CPConditionalExpression alloc] initWithPredicate:aPredicate trueExpression:trueExpression falseExpression:falseExpression]; +} + // Getting Information About an Expression /*! Returns the expression type for the receiver. diff --git a/Foundation/CPPredicate/_CPPredicate.j b/Foundation/CPPredicate/_CPPredicate.j index 56bfcd643..57192ab65 100644 --- a/Foundation/CPPredicate/_CPPredicate.j +++ b/Foundation/CPPredicate/_CPPredicate.j @@ -751,11 +751,35 @@ subpredicate = [self parsePredicate]; if (![self scanString:@")" intoString:NULL]) - CPRaiseParseError(self, @"expression"); + CPRaiseParseError(self, @"predicate"); return [[_CPSubqueryExpression alloc] initWithExpression:collection usingIteratorExpression:variableExpression predicate:subpredicate]; } + if ([self scanString:@"TERNARY" intoString:NULL]) + { + if (![self scanString:@"(" intoString:NULL]) + CPRaiseParseError(self, @"expression"); + + var predicate, + trueExpression, + falseExpression; + + predicate = [self parsePredicate]; + if (![self scanString:@"," intoString:NULL]) + CPRaiseParseError(self, @"predicate"); + + trueExpression = [self parseExpression]; + if (![self scanString:@"," intoString:NULL]) + CPRaiseParseError(self, @"expression"); + + falseExpression = [self parseExpression]; + if (![self scanString:@")" intoString:NULL]) + CPRaiseParseError(self, @"expression"); + + return [CPExpression expressionForConditional:predicate trueExpression:trueExpression falseExpression:falseExpression]; + } + if ([self scanString:@"FUNCTION" intoString:NULL]) { if (![self scanString:@"(" intoString:NULL]) diff --git a/Tests/Foundation/CPPredicateTest.j b/Tests/Foundation/CPPredicateTest.j index 2d6f8210a..470cbc722 100644 --- a/Tests/Foundation/CPPredicateTest.j +++ b/Tests/Foundation/CPPredicateTest.j @@ -86,6 +86,10 @@ } arguments:nil]; [self assertNotNull:expression_block message:"Block Expression should not be nil"]; + + var expression_conditional = [CPExpression expressionForConditional:[CPPredicate predicateWithValue:YES] trueExpression:expression_minusset falseExpression:expression_block]; + + [self assertNotNull:expression_conditional message:"Conditional Expression should not be nil"]; } - (void)testSetExpressionEvaluation @@ -167,6 +171,14 @@ [self assertTrue:([eval isEqual:"OBJA-B"]) message:"'" + [expression description] + "' result is "+ eval + "but should be " + "OBJA-B"]; } +- (void)testConditionalExpressionEvaluation +{ + var expression = [CPExpression expressionForConditional:[CPPredicate predicateWithFormat:"SELF = $variable"] trueExpression:[CPExpression expressionForConstantValue:"TRUE_EXP"] falseExpression:[CPExpression expressionForConstantValue:"FALSE_EXP"]]; + + var eval = [expression expressionValueWithObject:"OBJ" context:@{"variable":"OBJ"}]; + [self assertTrue:([eval isEqual:"TRUE_EXP"]) message:"'" + [expression description] + "' result is "+ eval + "but should be " + "TRUE_EXP"]; +} + - (void)testOptions { var pred = [[CPComparisonPredicate alloc] initWithLeftExpression:[CPExpression expressionForConstantValue:@"àa"] rightExpression:[CPExpression expressionForConstantValue:@"aà"] modifier:CPDirectPredicateModifier type:CPLikePredicateOperatorType options:3]; @@ -401,6 +413,9 @@ predicate = [CPPredicate predicateWithFormat:@"a UNION {'b'} = {'a','b'}"]; var left = [[predicate leftExpression] expressionValueWithObject:object context:nil]; [self assertTrue:[left isEqualToSet:result] message:"Expression eval " + left + " should be " + result]; + + predicate = [CPPredicate predicateWithFormat:@"TERNARY(Record1.Children[SIZE] = 1, 'Single', Record1.Name) = 'John'"]; + [self assertTrue:[predicate evaluateWithObject:dict] message:"Predicate " + predicate + " should evaluate to TRUE"]; } - (void)testExpressionAndPredicateIsEqual @@ -459,6 +474,10 @@ pred1 = [CPPredicate predicateWithFormat:@"a = 'a' AND b = 'b'"]; pred2 = [CPPredicate predicateWithFormat:@"a = 'a' AND b = 'b'"]; [self assert:pred1 equals:pred2]; + + pred1 = [CPPredicate predicateWithFormat:@"TERNARY(Record1.Children[FIRST] BEGINSWITH Record1.Name, 'SAME', 'DIFFERENT') = 'DIFFERENT'"]; + pred2 = [CPPredicate predicateWithFormat:@"TERNARY(Record1.Children[FIRST] BEGINSWITH Record1.Name, 'SAME', 'DIFFERENT') = 'DIFFERENT'"]; + [self assert:pred1 equals:pred2]; } - (void)testExpressionAndPredicateIsNotEqualToNil @@ -510,6 +529,10 @@ [self assert:pred1 notEqual:nil]; + pred1 = [CPPredicate predicateWithFormat:@"TERNARY(Record1.Children[FIRST] BEGINSWITH Record1.Name, 'SAME', 'DIFFERENT') = 'DIFFERENT'"]; + + [self assert:pred1 notEqual:nil]; + pred1 = [CPPredicate predicateWithFormat:@"$x CONTAINS 'a'"]; [self assert:pred1 notEqual:nil]; From e72a42fd2d205ed741bdc83123a2c7516b2926e1 Mon Sep 17 00:00:00 2001 From: cacaodev Date: Sun, 27 Dec 2015 19:28:03 +0100 Subject: [PATCH 03/16] FIXED: block & conditional expressions: support for CPPredicate -predicateWithSubstitutionVariables: --- Foundation/CPPredicate/_CPBlockExpression.j | 12 ++++++++++++ Foundation/CPPredicate/_CPConditionalExpression.j | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/Foundation/CPPredicate/_CPBlockExpression.j b/Foundation/CPPredicate/_CPBlockExpression.j index 109d5d517..4b1472e1a 100644 --- a/Foundation/CPPredicate/_CPBlockExpression.j +++ b/Foundation/CPPredicate/_CPBlockExpression.j @@ -75,6 +75,18 @@ return _block(object, args, context); } +- (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)bindings +{ + var args = []; + + [_arguments enumerateObjectsUsingBlock:function(exp, idx) + { + [args addObject:[exp _expressionWithSubstitutionVariables:bindings]]; + }]; + + return [[_CPBlockExpression alloc] initWithBlock:_block arguments:args]; +} + - (CPString)description { return [CPString stringWithFormat:@"Block(0x%@)", [CPString stringWithHash:[self UID]]]; diff --git a/Foundation/CPPredicate/_CPConditionalExpression.j b/Foundation/CPPredicate/_CPConditionalExpression.j index 6e636ec29..1f939a64e 100644 --- a/Foundation/CPPredicate/_CPConditionalExpression.j +++ b/Foundation/CPPredicate/_CPConditionalExpression.j @@ -76,6 +76,15 @@ return [exp expressionValueWithObject:object context:context]; } +- (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)bindings +{ + var s_predicate = [_predicate predicateWithSubstitutionVariables:bindings], + s_trueExp = [_trueExpression _expressionWithSubstitutionVariables:bindings], + s_falseExp = [_falseExpression _expressionWithSubstitutionVariables:bindings]; + + return [[_CPConditionalExpression alloc] initWithPredicate:s_predicate trueExpression:s_trueExp falseExpression:s_falseExp]; +} + - (CPString)description { return [CPString stringWithFormat:@"TERNARY(%@,%@,%@)", [_predicate predicateFormat], [_trueExpression description], [_falseExpression description]]; From 117ac0f5d60f0637f435d61e2bb4b0e5fd68d352 Mon Sep 17 00:00:00 2001 From: cacaodev Date: Sun, 27 Dec 2015 19:31:05 +0100 Subject: [PATCH 04/16] FIXED: replace js Array.map function with capp API Also fixed BlockExpression description to match a little more with cocoa impl. --- Foundation/CPPredicate/_CPBlockExpression.j | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Foundation/CPPredicate/_CPBlockExpression.j b/Foundation/CPPredicate/_CPBlockExpression.j index 4b1472e1a..10a007205 100644 --- a/Foundation/CPPredicate/_CPBlockExpression.j +++ b/Foundation/CPPredicate/_CPBlockExpression.j @@ -65,12 +65,10 @@ { var args = []; - if (_arguments !== nil) + [_arguments enumerateObjectsUsingBlock:function(exp, idx) { - args = _arguments.map(function(expression){ - return [expression expressionValueWithObject:object context:context]; - }); - } + [args addObject:[exp expressionValueWithObject:object context:context]]; + }]; return _block(object, args, context); } @@ -89,7 +87,7 @@ - (CPString)description { - return [CPString stringWithFormat:@"Block(0x%@)", [CPString stringWithHash:[self UID]]]; + return [CPString stringWithFormat:@"Block(function, %@)", [_arguments description]]; } @end \ No newline at end of file From f1591e593a2e62cf4ed26bc14cf140b03c0a2801 Mon Sep 17 00:00:00 2001 From: cacaodev Date: Sun, 27 Dec 2015 22:03:35 +0100 Subject: [PATCH 05/16] Coding style --- Foundation/CPPredicate/_CPConditionalExpression.j | 12 ++++++------ Foundation/CPPredicate/_CPPredicate.j | 7 +++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Foundation/CPPredicate/_CPConditionalExpression.j b/Foundation/CPPredicate/_CPConditionalExpression.j index 1f939a64e..86926e392 100644 --- a/Foundation/CPPredicate/_CPConditionalExpression.j +++ b/Foundation/CPPredicate/_CPConditionalExpression.j @@ -70,19 +70,19 @@ - (id)expressionValueWithObject:(id)object context:(CPDictionary)context { - var eval = [_predicate evaluateWithObject:object substitutionVariables:context]; - var exp = eval ? _trueExpression : _falseExpression; + var eval = [_predicate evaluateWithObject:object substitutionVariables:context], + exp = eval ? _trueExpression : _falseExpression; return [exp expressionValueWithObject:object context:context]; } - (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)bindings { - var s_predicate = [_predicate predicateWithSubstitutionVariables:bindings], - s_trueExp = [_trueExpression _expressionWithSubstitutionVariables:bindings], - s_falseExp = [_falseExpression _expressionWithSubstitutionVariables:bindings]; + var predicate = [_predicate predicateWithSubstitutionVariables:bindings], + trueExp = [_trueExpression _expressionWithSubstitutionVariables:bindings], + falseExp = [_falseExpression _expressionWithSubstitutionVariables:bindings]; - return [[_CPConditionalExpression alloc] initWithPredicate:s_predicate trueExpression:s_trueExp falseExpression:s_falseExp]; + return [[_CPConditionalExpression alloc] initWithPredicate:predicate trueExpression:trueExp falseExpression:falseExp]; } - (CPString)description diff --git a/Foundation/CPPredicate/_CPPredicate.j b/Foundation/CPPredicate/_CPPredicate.j index 57192ab65..34001c415 100644 --- a/Foundation/CPPredicate/_CPPredicate.j +++ b/Foundation/CPPredicate/_CPPredicate.j @@ -744,10 +744,12 @@ if (![self scanString:@"," intoString:NULL]) CPRaiseParseError(self, @"expression"); + variableExpression = [self parseExpression]; if (![self scanString:@"," intoString:NULL]) CPRaiseParseError(self, @"expression"); + subpredicate = [self parsePredicate]; if (![self scanString:@")" intoString:NULL]) @@ -761,19 +763,20 @@ if (![self scanString:@"(" intoString:NULL]) CPRaiseParseError(self, @"expression"); - var predicate, + var predicate = [self parsePredicate], trueExpression, falseExpression; - predicate = [self parsePredicate]; if (![self scanString:@"," intoString:NULL]) CPRaiseParseError(self, @"predicate"); trueExpression = [self parseExpression]; + if (![self scanString:@"," intoString:NULL]) CPRaiseParseError(self, @"expression"); falseExpression = [self parseExpression]; + if (![self scanString:@")" intoString:NULL]) CPRaiseParseError(self, @"expression"); From 36c417458df2f18554f8ead1e3b0ca045270089a Mon Sep 17 00:00:00 2001 From: cacaodev Date: Sun, 27 Dec 2015 22:08:30 +0100 Subject: [PATCH 06/16] STYLE: replaced expression methods with accessors Also added methods and documentation for the new methods: -trueExpression, -falseExpression and -expressionBlock. Completed documentation for -predicate and -arguments. --- .../CPPredicate/_CPAggregateExpression.j | 7 +--- Foundation/CPPredicate/_CPBlockExpression.j | 14 +------ .../CPPredicate/_CPConditionalExpression.j | 21 ++-------- .../CPPredicate/_CPConstantValueExpression.j | 7 +--- Foundation/CPPredicate/_CPExpression.j | 39 +++++++++++++++++-- .../CPPredicate/_CPFunctionExpression.j | 14 +------ Foundation/CPPredicate/_CPSetExpression.j | 14 +------ .../CPPredicate/_CPSubqueryExpression.j | 14 +------ .../CPPredicate/_CPVariableExpression.j | 7 +--- 9 files changed, 50 insertions(+), 87 deletions(-) diff --git a/Foundation/CPPredicate/_CPAggregateExpression.j b/Foundation/CPPredicate/_CPAggregateExpression.j index 688166398..2216c959c 100644 --- a/Foundation/CPPredicate/_CPAggregateExpression.j +++ b/Foundation/CPPredicate/_CPAggregateExpression.j @@ -25,7 +25,7 @@ @implementation _CPAggregateExpression : CPExpression { - CPArray _aggregate; + CPArray _aggregate @accessors(getter=collection); } - (id)initWithAggregate:(CPArray)collection @@ -48,11 +48,6 @@ return YES; } -- (id)collection -{ - return _aggregate; -} - - (id)expressionValueWithObject:(id)object context:(CPDictionary)context { var eval_array = [CPArray array], diff --git a/Foundation/CPPredicate/_CPBlockExpression.j b/Foundation/CPPredicate/_CPBlockExpression.j index 10a007205..299f250ee 100644 --- a/Foundation/CPPredicate/_CPBlockExpression.j +++ b/Foundation/CPPredicate/_CPBlockExpression.j @@ -23,8 +23,8 @@ @implementation _CPBlockExpression : CPExpression { - Function _block; - CPArray _arguments; + Function _block @accessors(getter=expressionBlock); + CPArray _arguments @accessors(getter=arguments); } - (id)initWithBlock:(Function)aBlock arguments:(CPArray)arguments @@ -51,16 +51,6 @@ return YES; } -- (Function)expressionBlock -{ - return _block; -} - -- (CPArray)arguments -{ - return _arguments; -} - - (id)expressionValueWithObject:(id)object context:(CPDictionary)context { var args = []; diff --git a/Foundation/CPPredicate/_CPConditionalExpression.j b/Foundation/CPPredicate/_CPConditionalExpression.j index 86926e392..8fc544580 100644 --- a/Foundation/CPPredicate/_CPConditionalExpression.j +++ b/Foundation/CPPredicate/_CPConditionalExpression.j @@ -23,9 +23,9 @@ @implementation _CPConditionalExpression : CPExpression { - CPPredicate _predicate; - CPExpression _trueExpression; - CPExpression _falseExpression; + CPPredicate _predicate @accessors(getter=predicate); + CPExpression _trueExpression @accessors(getter=trueExpression); + CPExpression _falseExpression @accessors(getter=falseExpression); } - (id)initWithPredicate:(CPPredicate)aPredicate trueExpression:(CPExpression)trueExpression falseExpression:(CPExpression)falseExpression @@ -53,21 +53,6 @@ return YES; } -- (CPPredicate)predicate -{ - return _predicate; -} - -- (CPExpression)trueExpression -{ - return _trueExpression; -} - -- (CPExpression)falseExpression -{ - return _falseExpression; -} - - (id)expressionValueWithObject:(id)object context:(CPDictionary)context { var eval = [_predicate evaluateWithObject:object substitutionVariables:context], diff --git a/Foundation/CPPredicate/_CPConstantValueExpression.j b/Foundation/CPPredicate/_CPConstantValueExpression.j index 617c9b10a..81fd60b2e 100644 --- a/Foundation/CPPredicate/_CPConstantValueExpression.j +++ b/Foundation/CPPredicate/_CPConstantValueExpression.j @@ -27,7 +27,7 @@ @implementation _CPConstantValueExpression : CPExpression { - id _value; + id _value @accessors(getter=constantValue); } - (id)initWithValue:(id)value @@ -51,11 +51,6 @@ return YES; } -- (id)constantValue -{ - return _value; -} - - (id)expressionValueWithObject:(id)object context:(CPDictionary)context { return _value; diff --git a/Foundation/CPPredicate/_CPExpression.j b/Foundation/CPPredicate/_CPExpression.j index b26707fc0..8a671c5b2 100644 --- a/Foundation/CPPredicate/_CPExpression.j +++ b/Foundation/CPPredicate/_CPExpression.j @@ -350,7 +350,7 @@ A dictionary that the expression can use to store temporary state for one predic /*! Returns the arguments for the receiver. - @return The arguments for the receiver—that is, the array of expressions that will be passed as parameters during invocation of the selector on the operand of a function expression. + @return The arguments for the receiver—that is, the array of expressions that will be passed as parameters during invocation of the selector on the operand of a function expression or as a parameter of the block of a block expression. This method raises an exception if it is not applicable to the receiver. */ - (CPArray)arguments @@ -371,8 +371,8 @@ A dictionary that the expression can use to store temporary state for one predic } /*! - Returns the predicate in a subquery expression. - @return The predicate in a subquery expression.. + Returns the predicate in a subquery expression or a conditional expression. + @return The predicate in a subquery expression or a conditional expression. This method raises an exception if it is not applicable to the receiver. */ - (CPPredicate)predicate @@ -414,6 +414,39 @@ A dictionary that the expression can use to store temporary state for one predic return nil; } +/*! + Returns the block of a block expression. + @return The block of a block expression. + This method raises an exception if it is not applicable to the receiver. +*/ +- (Function)expressionBlock +{ + _CPRaiseInvalidAbstractInvocation(self, _cmd); + return nil; +} + +/*! + Returns the true expression of a conditional expression. + @return The true expression of a conditional expression. + This method raises an exception if it is not applicable to the receiver. +*/ +- (CPExpression)trueExpression +{ + _CPRaiseInvalidAbstractInvocation(self, _cmd); + return nil; +} + +/*! + Returns the false expression of a conditional expression. + @return The false expression of a conditional expression. + This method raises an exception if it is not applicable to the receiver. +*/ +- (CPExpression)falseExpression +{ + _CPRaiseInvalidAbstractInvocation(self, _cmd); + return nil; +} + - (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)variables { return self; diff --git a/Foundation/CPPredicate/_CPFunctionExpression.j b/Foundation/CPPredicate/_CPFunctionExpression.j index 394c93612..32790d4e4 100644 --- a/Foundation/CPPredicate/_CPFunctionExpression.j +++ b/Foundation/CPPredicate/_CPFunctionExpression.j @@ -28,9 +28,9 @@ @implementation _CPFunctionExpression : CPExpression { - CPExpression _operand; + CPExpression _operand @accessors(getter=operand); SEL _selector; - CPArray _arguments; + CPArray _arguments @accessors(getter=arguments); int _argc; int _maxargs; } @@ -88,16 +88,6 @@ return [self _function]; } -- (CPArray)arguments -{ - return _arguments; -} - -- (CPExpression)operand -{ - return _operand; -} - - (id)expressionValueWithObject:(id)object context:(CPDictionary)context { var target = [_operand expressionValueWithObject:object context:context], diff --git a/Foundation/CPPredicate/_CPSetExpression.j b/Foundation/CPPredicate/_CPSetExpression.j index a0e9df3bb..707486b32 100644 --- a/Foundation/CPPredicate/_CPSetExpression.j +++ b/Foundation/CPPredicate/_CPSetExpression.j @@ -25,8 +25,8 @@ @implementation _CPSetExpression : CPExpression { - CPExpression _left; - CPExpression _right; + CPExpression _left @accessors(getter=leftExpression); + CPExpression _right @accessors(getter=rightExpression); } - (id)initWithType:(int)type left:(CPExpression)left right:(CPExpression)right @@ -88,16 +88,6 @@ return self; } -- (CPExpression)leftExpression -{ - return _left; -} - -- (CPExpression)rightExpression -{ - return _right; -} - - (CPString)description { var desc; diff --git a/Foundation/CPPredicate/_CPSubqueryExpression.j b/Foundation/CPPredicate/_CPSubqueryExpression.j index e8f648403..d69483d2c 100644 --- a/Foundation/CPPredicate/_CPSubqueryExpression.j +++ b/Foundation/CPPredicate/_CPSubqueryExpression.j @@ -26,9 +26,9 @@ @implementation _CPSubqueryExpression : CPExpression { - CPExpression _collection; + CPExpression _collection @accessors(getter=collection); CPExpression _variableExpression; - CPPredicate _subpredicate; + CPPredicate _subpredicate @accessors(getter=predicate); } - (id)initWithExpression:(CPExpression)collection usingIteratorVariable:(CPString)variable predicate:(CPPredicate)subpredicate @@ -81,21 +81,11 @@ return YES; } -- (CPExpression)collection -{ - return _collection; -} - - (id)copy { return [[_CPSubqueryExpression alloc] initWithExpression:[_collection copy] usingIteratorExpression:[_variableExpression copy] predicate:[_subpredicate copy]]; } -- (CPPredicate)predicate -{ - return _subpredicate; -} - - (CPString)description { return [self predicateFormat]; diff --git a/Foundation/CPPredicate/_CPVariableExpression.j b/Foundation/CPPredicate/_CPVariableExpression.j index 910ef23d5..a33911222 100644 --- a/Foundation/CPPredicate/_CPVariableExpression.j +++ b/Foundation/CPPredicate/_CPVariableExpression.j @@ -29,7 +29,7 @@ @implementation _CPVariableExpression : CPExpression { - CPString _variable; + CPString _variable @accessors(getter=variable); } - (id)initWithVariable:(CPString)variable @@ -54,11 +54,6 @@ return YES; } -- (CPString)variable -{ - return _variable; -} - - (id)expressionValueWithObject:object context:(CPDictionary)context { var expression = [self _expressionWithSubstitutionVariables:context]; From 0f30d7744d3a2d98a6f430526f90a9a47bb4146b Mon Sep 17 00:00:00 2001 From: Didier Korthoudt Date: Tue, 19 Jan 2016 07:07:45 +0100 Subject: [PATCH 07/16] FIXED: CPTrackingArea incorrect handling of view hierarchy updates while processing events --- AppKit/CPView.j | 40 ++-- AppKit/CPWindow/CPWindow.j | 384 ++++++++++++++++++++++--------------- Tests/AppKit/CPViewTest.j | 209 ++++++++++++++++++++ 3 files changed, 463 insertions(+), 170 deletions(-) diff --git a/AppKit/CPView.j b/AppKit/CPView.j index 5b8d66007..0df313b33 100644 --- a/AppKit/CPView.j +++ b/AppKit/CPView.j @@ -805,13 +805,13 @@ var CPViewHighDPIDrawingEnabled = YES; [_window _noteUnregisteredDraggedTypes:_registeredDraggedTypes]; [aWindow _noteRegisteredDraggedTypes:_registeredDraggedTypes]; } - + // View must be removed from the current window viewsWithTrackingAreas if (_window && (_trackingAreas.length > 0)) [_window _removeTrackingAreaView:self]; _window = aWindow; - + if (_window) { var owners; @@ -3456,16 +3456,16 @@ setBoundsOrigin: // Consistency check if (!trackingArea || [_trackingAreas containsObjectIdenticalTo:trackingArea]) return; - + if ([trackingArea view]) [CPException raise:CPInternalInconsistencyException reason:"Tracking area has already been added to another view."]; [_trackingAreas addObject:trackingArea]; [trackingArea setView:self]; - + if (_window) [_window _addTrackingArea:trackingArea]; - + [trackingArea _updateWindowRect]; } @@ -3474,14 +3474,14 @@ setBoundsOrigin: // Consistency check if (!trackingArea) return; - + if (![_trackingAreas containsObjectIdenticalTo:trackingArea]) [CPException raise:CPInternalInconsistencyException reason:"Trying to remove unreferenced trackingArea"]; [self _removeTrackingArea:trackingArea]; } -/*! +/*! Invoked automatically when the view’s geometry changes such that its tracking areas need to be recalculated. You should override this method to remove out of date tracking areas and add recomputed tracking areas; @@ -3527,7 +3527,7 @@ setBoundsOrigin: { if (_window) [_window _removeTrackingArea:trackingArea]; - + [trackingArea setView:nil]; [_trackingAreas removeObjectIdenticalTo:trackingArea]; } @@ -3535,16 +3535,16 @@ setBoundsOrigin: - (void)_updateTrackingAreas { _inhibitUpdateTrackingAreas = YES; - + [self _recursivelyUpdateTrackingAreas]; - + _inhibitUpdateTrackingAreas = NO; } - (void)_recursivelyUpdateTrackingAreas { [self _updateTrackingAreasForOwners:[self _calcTrackingAreaOwners]]; - + for (var i = 0; i < _subviews.length; i++) [_subviews[i] _recursivelyUpdateTrackingAreas]; } @@ -3554,25 +3554,25 @@ setBoundsOrigin: // First search all owners that must be notified // Remark: 99.99% of time, the only owner will be the view itself // In the same time, update the rects of InVisibleRect tracking areas - + var owners = []; - + for (var i = 0; i < _trackingAreas.length; i++) { var trackingArea = _trackingAreas[i]; - + if ([trackingArea options] & CPTrackingInVisibleRect) [trackingArea _updateWindowRect]; - + else { var owner = [trackingArea owner]; - + if (![owners containsObjectIdenticalTo:owner]) [owners addObject:owner]; } } - + return owners; } @@ -3603,7 +3603,7 @@ var CPViewAutoresizingMaskKey = @"CPViewAutoresizingMask", CPViewScaleKey = @"CPViewScaleKey", CPViewSizeScaleKey = @"CPViewSizeScaleKey", CPViewIsScaledKey = @"CPViewIsScaledKey", - CPViewAppearanceKey = @"CPViewAppearanceKey"; + CPViewAppearanceKey = @"CPViewAppearanceKey", CPViewTrackingAreasKey = @"CPViewTrackingAreasKey"; @implementation CPView (CPCoding) @@ -3633,10 +3633,10 @@ var CPViewAutoresizingMaskKey = @"CPViewAutoresizingMask", if (self) { _trackingAreas = [aCoder decodeObjectForKey:CPViewTrackingAreasKey]; - + if (!_trackingAreas) _trackingAreas = []; - + // We have to manually check because it may be 0, so we can't use || _tag = [aCoder containsValueForKey:CPViewTagKey] ? [aCoder decodeIntForKey:CPViewTagKey] : -1; _identifier = [aCoder decodeObjectForKey:CPReuseIdentifierKey]; diff --git a/AppKit/CPWindow/CPWindow.j b/AppKit/CPWindow/CPWindow.j index 79b4b7324..fad98a966 100644 --- a/AppKit/CPWindow/CPWindow.j +++ b/AppKit/CPWindow/CPWindow.j @@ -198,8 +198,13 @@ var CPWindowActionMessageKeys = [ CPView _contentView; CPView _toolbarView; + BOOL _handlingTrackingAreaEvent; + BOOL _restartHandlingTrackingAreaEvent; + CPArray _previousMouseEnteredStack; + CPArray _previousCursorUpdateStack; CPArray _mouseEnteredStack; CPArray _cursorUpdateStack; + CPArray _queuedEvents; CPArray _trackingAreaViews; id _activeCursorTrackingArea; CPArray _queuedTrackingEvents; @@ -340,9 +345,14 @@ CPTexturedBackgroundWindowMask [self setLevel:CPNormalWindowLevel]; + _handlingTrackingAreaEvent = NO; + _restartHandlingTrackingAreaEvent = NO; _trackingAreaViews = []; + _previousMouseEnteredStack = []; + _previousCursorUpdateStack = []; _mouseEnteredStack = []; _cursorUpdateStack = []; + _queuedEvents = []; _queuedTrackingEvents = []; _activeCursorTrackingArea = nil; @@ -1980,7 +1990,7 @@ CPTexturedBackgroundWindowMask // First, we search for any tracking area requesting CPTrackingEnabledDuringMouseDrag. // At the same time, we update the entered stack. [self _handleTrackingAreaEvent:anEvent]; - + // Normal mouseDragged workflow if (!_leftMouseDownView) return [[_windowView hitTest:point] mouseDragged:anEvent]; @@ -2004,7 +2014,7 @@ CPTexturedBackgroundWindowMask // Ignore mouse moves for parents of sheets if (!_acceptsMouseMovedEvents || sheet) return; - + [self _handleTrackingAreaEvent:anEvent]; } } @@ -3824,7 +3834,7 @@ var interpolate = function(fromValue, toValue, progress) - (void)_addTrackingAreaView:(CPView)aView { var trackingAreas = [aView trackingAreas]; - + for (var i = 0; i < trackingAreas.length; i++) [self _addTrackingArea:trackingAreas[i]]; } @@ -3832,7 +3842,7 @@ var interpolate = function(fromValue, toValue, progress) - (void)_removeTrackingAreaView:(CPView)aView { var trackingAreas = [aView trackingAreas]; - + for (var i = 0; i < trackingAreas.length; i++) [self _removeTrackingArea:trackingAreas[i]]; } @@ -3840,63 +3850,141 @@ var interpolate = function(fromValue, toValue, progress) - (void)_addTrackingArea:(CPTrackingArea)trackingArea { var trackingAreaView = [trackingArea view]; - + if (![_trackingAreaViews containsObjectIdenticalTo:trackingAreaView]) [_trackingAreaViews addObject:trackingAreaView]; - - // If CPTrackingAssumeInside option is set, put the tracking area in the _mouseEnteredStack - - if ([trackingArea options] & CPTrackingAssumeInside) - [_mouseEnteredStack addObject:trackingArea]; + + // If CPTrackingAssumeInside option is set, insert the tracking area in the events management system + // in order to have the first event sent only when mouse leaves the tracking area + + [self _insertTrackingArea:trackingArea assumeInside:([trackingArea options] & CPTrackingAssumeInside)]; } - (void)_removeTrackingArea:(CPTrackingArea)trackingArea { // If mouse is in the tracking area, we remove it from the stack to avoid to fire a future mouseExited event - - [_mouseEnteredStack removeObjectIdenticalTo:trackingArea]; - + + [self _purgeTrackingArea:trackingArea]; + var trackingAreaView = [trackingArea view]; - + [_trackingAreaViews removeObjectIdenticalTo:trackingAreaView]; } +- (void)_insertTrackingArea:(CPTrackingArea)trackingArea assumeInside:(BOOL)assumeInside +{ + if (_handlingTrackingAreaEvent) + _restartHandlingTrackingAreaEvent = YES; + + if (assumeInside) + if (_handlingTrackingAreaEvent) + [_mouseEnteredStack addObject:trackingArea]; + else + [_previousMouseEnteredStack addObject:trackingArea]; +} + +- (void)_purgeTrackingArea:(CPTrackingArea)trackingArea +{ + if (_handlingTrackingAreaEvent) + { + [_mouseEnteredStack removeObjectIdenticalTo:trackingArea]; + + var i = _queuedEvents.length; + + while (i--) + if ([_queuedEvents[i] trackingArea] === trackingArea) + [_queuedEvents removeObjectAtIndex:i]; + + _cursorUpdateStack = []; + _activeCursorTrackingArea = nil; + } + else + { + [_previousMouseEnteredStack removeObjectIdenticalTo:trackingArea]; + [_previousCursorUpdateStack removeObjectIdenticalTo:trackingArea]; + } +} + - (void)_handleTrackingAreaEvent:(CPEvent)anEvent { - var mouseEnteredStack = [], - cursorUpdateStack = [], - point = [anEvent locationInWindow], - dragging = ([anEvent type] !== CPMouseMoved); + _handlingTrackingAreaEvent = YES; - // Handle mouse entering tracking areas (and calc mouseEnteredStack and cursorUpdateStack) - - [self _handleMouseMovedAndEnteredEventsForEvent:anEvent atPoint:point dragging:dragging mouseEnteredStack:mouseEnteredStack cursorUpdateStack:cursorUpdateStack]; + var point = [anEvent locationInWindow], + dragging = ([anEvent type] !== CPMouseMoved); - // Handle mouse exiting tracking areas - - [self _handleMouseExitedEventsForEvent:anEvent atPoint:point dragging:dragging mouseEnteredStack:mouseEnteredStack]; - - // Cursor update - - if (cursorUpdateStack.length > 0) + do { - [self _handleCursorUpdateEventsForEvent:anEvent atPoint:point dragging:dragging cursorUpdateStack:cursorUpdateStack]; - } - else if (!dragging) - { - // Here, we are outsite the window content view tracking area, so let _windowView set the cursor (resize cursor, ...) + // Initialize this run + _restartHandlingTrackingAreaEvent = NO; - [_windowView setCursorForLocation:point resizing:NO]; - _activeCursorTrackingArea = nil; - } + _mouseEnteredStack = []; + _cursorUpdateStack = []; - // Prepare for next call - - _mouseEnteredStack = mouseEnteredStack; - _cursorUpdateStack = cursorUpdateStack; + // Important remark: we must queue events to avoid running conditions when a view uses a mouse event to modify view hierarchy + + _queuedEvents = []; + + // Handle mouse entering tracking areas (and calc _mouseEnteredStack and _cursorUpdateStack) + + [self _handleMouseMovedAndEnteredEventsForEvent:anEvent atPoint:point dragging:dragging]; + + // Handle mouse exiting tracking areas + + [self _handleMouseExitedEventsForEvent:anEvent atPoint:point dragging:dragging]; + + // Cursor update + + if (_cursorUpdateStack.length > 0) + { + [self _handleCursorUpdateEventsForEvent:anEvent atPoint:point dragging:dragging]; + } + else if (!dragging) + { + // Here, we are outside the window content view tracking area, so let _windowView set the cursor (resize cursor, ...) + + [_windowView setCursorForLocation:point resizing:NO]; + _activeCursorTrackingArea = nil; + } + + // Send all queued events + // Important remark : as an event can modify the view hierarchy, the events queue could be modified while processing it + + while (_queuedEvents.length > 0) + { + var queuedEvent = _queuedEvents[0], + trackingArea = [queuedEvent trackingArea], + trackingOwner = [trackingArea owner]; + + switch ([queuedEvent type]) + { + case CPMouseEntered: + [trackingOwner mouseEntered:queuedEvent]; + break; + + case CPMouseExited: + [trackingOwner mouseExited:queuedEvent]; + break; + + case CPCursorUpdate: + [trackingOwner cursorUpdate:queuedEvent]; + break; + } + + if (queuedEvent === _queuedEvents[0]) + [_queuedEvents removeObjectAtIndex:0]; + } + + // Prepare for next call + + _previousMouseEnteredStack = _mouseEnteredStack; + _previousCursorUpdateStack = _cursorUpdateStack; + } + while (_restartHandlingTrackingAreaEvent) + + _handlingTrackingAreaEvent = NO; } -- (void)_handleMouseMovedAndEnteredEventsForEvent:(CPEvent)anEvent atPoint:(CGPoint)point dragging:(BOOL)dragging mouseEnteredStack:(CPArray)mouseEnteredStack cursorUpdateStack:(CPArray)cursorUpdateStack +- (void)_handleMouseMovedAndEnteredEventsForEvent:(CPEvent)anEvent atPoint:(CGPoint)point dragging:(BOOL)dragging { var isKeyWindow = [self isKeyWindow]; @@ -3923,9 +4011,9 @@ var interpolate = function(fromValue, toValue, progress) continue; } - [mouseEnteredStack addObject:aTrackingArea]; + [_mouseEnteredStack addObject:aTrackingArea]; - if ([_mouseEnteredStack containsObjectIdenticalTo:aTrackingArea]) + if ([_previousMouseEnteredStack containsObjectIdenticalTo:aTrackingArea]) { // Mouse was already in this rect so it's a mouseMoved @@ -3946,32 +4034,33 @@ var interpolate = function(fromValue, toValue, progress) if (dragging && !(trackingOptions & CPTrackingEnabledDuringMouseDrag)) [self _queueTrackingEvent:mouseEnteredEvent]; else - [[aTrackingArea owner] mouseEntered:mouseEnteredEvent]; + [_queuedEvents addObject:mouseEnteredEvent]; } if ((trackingOptions & CPTrackingCursorUpdate) && (trackingImplementedMethods & CPTrackingOwnerImplementsCursorUpdate)) - [cursorUpdateStack addObject:aTrackingArea]; + [_cursorUpdateStack addObject:aTrackingArea]; } } } -- (void)_handleMouseExitedEventsForEvent:(CPEvent)anEvent atPoint:(CGPoint)point dragging:(BOOL)dragging mouseEnteredStack:(CPArray)mouseEnteredStack +- (void)_handleMouseExitedEventsForEvent:(CPEvent)anEvent atPoint:(CGPoint)point dragging:(BOOL)dragging { - // Search for exited views (were in _mouseEnteredStack but no more in mouseEnteredStack) + // Search for exited views (were in _previousMouseEnteredStack but no more in _mouseEnteredStack) - for (var i = 0; i < _mouseEnteredStack.length; i++) + for (var i = 0; i < _previousMouseEnteredStack.length; i++) { - var aTrackingArea = _mouseEnteredStack[i], + var aTrackingArea = _previousMouseEnteredStack[i], trackingOptions = [aTrackingArea options]; - if ([mouseEnteredStack containsObjectIdenticalTo:aTrackingArea]) + if ([_mouseEnteredStack containsObjectIdenticalTo:aTrackingArea]) continue; // Mouse is no more in this area so it's a mouseExited if ((trackingOptions & CPTrackingMouseEnteredAndExited) && ([aTrackingArea implementedOwnerMethods] & CPTrackingOwnerImplementsMouseExited)) { - var mouseExitedEvent = [CPEvent enterExitEventWithType:CPMouseExited + var theView = [aTrackingArea owner], + mouseExitedEvent = [CPEvent enterExitEventWithType:CPMouseExited location:point modifierFlags:[anEvent modifierFlags] timestamp:[anEvent timestamp] @@ -3983,132 +4072,127 @@ var interpolate = function(fromValue, toValue, progress) if (dragging && !(trackingOptions & CPTrackingEnabledDuringMouseDrag)) [self _queueTrackingEvent:mouseExitedEvent]; else - [[aTrackingArea owner] mouseExited:mouseExitedEvent]; + [_queuedEvents addObject:mouseExitedEvent]; } - // If this is the active cursor area, we reset _cursorUpdateStack so a new active area will be computed + // If this is the active cursor area, we reset _previousCursorUpdateStack so a new active area will be computed if (aTrackingArea === _activeCursorTrackingArea) { - _cursorUpdateStack = []; + _previousCursorUpdateStack = []; _activeCursorTrackingArea = nil; } } } -- (void)_handleCursorUpdateEventsForEvent:(CPEvent)anEvent atPoint:(CGPoint)point dragging:(BOOL)dragging cursorUpdateStack:(CPArray)cursorUpdateStack +- (void)_handleCursorUpdateEventsForEvent:(CPEvent)anEvent atPoint:(CGPoint)point dragging:(BOOL)dragging { var overlappingTrackingAreas = []; - for (var i = 0; i < cursorUpdateStack.length; i++) + for (var i = 0; i < _cursorUpdateStack.length; i++) { - var aTrackingArea = cursorUpdateStack[i]; + var aTrackingArea = _cursorUpdateStack[i]; - if ((![_cursorUpdateStack containsObjectIdenticalTo:aTrackingArea]) || (aTrackingArea === _activeCursorTrackingArea)) + if ((![_previousCursorUpdateStack containsObjectIdenticalTo:aTrackingArea]) || (aTrackingArea === _activeCursorTrackingArea)) [overlappingTrackingAreas addObject:aTrackingArea]; } - var nbOverlappingTrackingAreas = overlappingTrackingAreas.length; + var frontmostTrackingArea = overlappingTrackingAreas[0], + frontmostView = [frontmostTrackingArea view]; - if (nbOverlappingTrackingAreas > 0) + for (var i = 1; i < overlappingTrackingAreas.length; i++) { - var frontmostTrackingArea = overlappingTrackingAreas[0], - frontmostView = [frontmostTrackingArea view]; + var aTrackingArea = overlappingTrackingAreas[i], + aView = [aTrackingArea view]; - for (var i = 1; i < nbOverlappingTrackingAreas; i++) + // First, if aView is _windowView, skip to next overlapping tracking area + // as _windowView can't be the frontmost view if there's multiple overlapping tracking areas. + + if (aView === _windowView) + continue; + + // Then, if frontmostView is _windowView, aView must become frontmostView + + if (frontmostView === _windowView) { - var aTrackingArea = overlappingTrackingAreas[i], - aView = [aTrackingArea view]; + frontmostTrackingArea = aTrackingArea; + frontmostView = aView; - // First, if aView is _windowView, skip to next overlapping tracking area - // as _windowView can't be the frontmost view if there's multiple overlapping tracking areas. - - if (aView === _windowView) - continue; - - // Then, if frontmostView is _windowView, aView must become frontmostView - - if (frontmostView === _windowView) - { - frontmostTrackingArea = aTrackingArea; - frontmostView = aView; - - continue; - } - - // Next verify if aView is a subview of frontmostView - // If so, it's our new frontmost view - - var searchingView = aView; - - while ((searchingView !== _contentView) && ([searchingView superview] !== frontmostView)) - searchingView = [searchingView superview]; - - if (searchingView !== _contentView) - { - frontmostTrackingArea = aTrackingArea; - frontmostView = aView; - - continue; - } - - // aView is not a subview of frontmostView - // Search in view hierarchy which one will be over the other - // (this is done by comparing their draw order) - - var firstView = frontmostView, - firstSuperview = [firstView superview]; - - while (firstView !== _contentView) - { - var secondView = aView, - secondSuperview = [secondView superview]; - - while ((secondSuperview !== _contentView) && (firstSuperview !== secondSuperview)) - { - secondView = secondSuperview; - secondSuperview = [secondView superview]; - } - - if (firstSuperview === secondSuperview) - break; - - firstView = firstSuperview; - firstSuperview = [firstView superview]; - } - - if (firstSuperview !== secondSuperview) - [CPException raise:CPInternalInconsistencyException reason:"Problem with view hierarchy"]; - - var firstSuperviewSubviews = [firstSuperview subviews], - firstViewIndex = [firstSuperviewSubviews indexOfObject:firstView], - secondViewIndex = [firstSuperviewSubviews indexOfObject:secondView]; - - if (secondViewIndex > firstViewIndex) - { - frontmostTrackingArea = aTrackingArea; - frontmostView = aView; - } + continue; } - if (frontmostTrackingArea !== _activeCursorTrackingArea) + // Next verify if aView is a subview of frontmostView + // If so, it's our new frontmost view + + var searchingView = aView; + + while ((searchingView !== _contentView) && ([searchingView superview] !== frontmostView)) + searchingView = [searchingView superview]; + + if (searchingView !== _contentView) { - var cursorUpdateEvent = [CPEvent enterExitEventWithType:CPCursorUpdate - location:point - modifierFlags:[anEvent modifierFlags] - timestamp:[anEvent timestamp] - windowNumber:_windowNumber - context:nil - eventNumber:-1 - trackingArea:frontmostTrackingArea]; + frontmostTrackingArea = aTrackingArea; + frontmostView = aView; - if (dragging) - [self _queueTrackingEvent:cursorUpdateEvent]; - else - [[frontmostTrackingArea owner] cursorUpdate:cursorUpdateEvent]; - - _activeCursorTrackingArea = frontmostTrackingArea; + continue; } + + // aView is not a subview of frontmostView + // Search in view hierarchy which one will be over the other + // (this is done by comparing their draw order) + + var firstView = frontmostView, + firstSuperview = [firstView superview]; + + while (firstView !== _contentView) + { + var secondView = aView, + secondSuperview = [secondView superview]; + + while ((secondSuperview !== _contentView) && (firstSuperview !== secondSuperview)) + { + secondView = secondSuperview; + secondSuperview = [secondView superview]; + } + + if (firstSuperview === secondSuperview) + break; + + firstView = firstSuperview; + firstSuperview = [firstView superview]; + } + + if (firstSuperview !== secondSuperview) + [CPException raise:CPInternalInconsistencyException reason:"Problem with view hierarchy"]; + + var firstSuperviewSubviews = [firstSuperview subviews], + firstViewIndex = [firstSuperviewSubviews indexOfObject:firstView], + secondViewIndex = [firstSuperviewSubviews indexOfObject:secondView]; + + if (secondViewIndex > firstViewIndex) + { + frontmostTrackingArea = aTrackingArea; + frontmostView = aView; + } + } + + if (frontmostTrackingArea !== _activeCursorTrackingArea) + { + var cursorUpdateEvent = [CPEvent enterExitEventWithType:CPCursorUpdate + location:point + modifierFlags:[anEvent modifierFlags] + timestamp:[anEvent timestamp] + windowNumber:_windowNumber + context:nil + eventNumber:-1 + trackingArea:frontmostTrackingArea]; + + if (dragging) + [self _queueTrackingEvent:cursorUpdateEvent]; + else + [_queuedEvents addObject:cursorUpdateEvent]; + + _activeCursorTrackingArea = frontmostTrackingArea; } } @@ -4181,7 +4265,7 @@ var interpolate = function(fromValue, toValue, progress) case CPMouseExited: [trackingOwner mouseExited:queuedEvent]; break; - + case CPCursorUpdate: [trackingOwner updateTrackingAreas]; diff --git a/Tests/AppKit/CPViewTest.j b/Tests/AppKit/CPViewTest.j index 426a6c086..740601c4c 100644 --- a/Tests/AppKit/CPViewTest.j +++ b/Tests/AppKit/CPViewTest.j @@ -1360,6 +1360,144 @@ var updateTrackingAreasCalls, } +- (void)testTrackingAreasLiveViewHierarchyModification +{ + // 1. viewB inside viewA with mouseEntered removing itself + + var viewA = [[CPTrackingAreaViewWithCursorUpdate alloc] initWithFrame:CGRectMake(20, 20, 40, 40)]; + var viewB = [[CPTrackingAreaViewLiveRemoval alloc] initWithFrame:CGRectMake(10, 10, 20, 20)]; + + [[window contentView] setSubviews:[CPArray arrayWithObject:viewA]]; + [viewA addSubview:viewB]; + + var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; + + var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; + var viewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewB userInfo:nil]; + + [viewA addTrackingArea:viewATrackingArea]; + [viewB addTrackingArea:viewBTrackingArea]; + + // Step 1.1 : enter viewA + + [self moveMouseAtPoint:CGPointMake(25, 25) dragging:NO]; + + [self assert:1 equals:mouseEnteredCalls message:@"1.1 There should be one and only one mouseEntered call"]; + [self assert:0 equals:mouseExitedCalls message:@"1.1 There should be no mouseExited call"]; + [self assert:0 equals:mouseMovedCalls message:@"1.1 There should be no mouseMoved call"]; + [self assert:1 equals:cursorUpdateCalls message:@"1.1 There should be one and only one cursorUpdate call"]; + + [self assert:viewA equals:involvedViewForMouseEntered message:@"1.1 viewA should receive mouseEntered"]; + [self assert:viewA equals:involvedViewForCursorUpdate message:@"1.1 viewA should receive cursorUpdate"]; + + // Step 1.2 : enter viewB + + [self moveMouseAtPoint:CGPointMake(40, 40) dragging:NO]; + + [self assert:1 equals:mouseEnteredCalls message:@"1.2 There should be one and only one mouseEntered call"]; + [self assert:0 equals:mouseExitedCalls message:@"1.2 There should be no mouseExited call"]; + [self assert:0 equals:mouseMovedCalls message:@"1.2 There should be no mouseMoved call"]; + [self assert:0 equals:cursorUpdateCalls message:@"1.2 There should be no cursorUpdate call"]; + + [self assert:viewB equals:involvedViewForMouseEntered message:@"1.2 viewB should receive mouseEntered"]; + + // Step 1.3 : move back to viewA (there should be no more viewB) + + [self moveMouseAtPoint:CGPointMake(25, 25) dragging:NO]; + + [self assert:0 equals:mouseEnteredCalls message:@"1.3 There should be no mouseEntered call"]; + [self assert:0 equals:mouseExitedCalls message:@"1.3 There should be no mouseExited call"]; + [self assert:0 equals:mouseMovedCalls message:@"1.3 There should be no mouseMoved call"]; + [self assert:1 equals:cursorUpdateCalls message:@"1.3 There should be one and only one cursorUpdate call"]; + + [self assert:viewA equals:involvedViewForCursorUpdate message:@"1.3 viewA should receive cursorUpdate"]; + + // Step 1.4 : exit viewA + + [self moveMouseAtPoint:CGPointMake(5, 5) dragging:NO]; + + [self assert:0 equals:mouseEnteredCalls message:@"1.4 There should be one and only one mouseEntered call"]; + [self assert:1 equals:mouseExitedCalls message:@"1.4 There should be one and only on mouseExited call"]; + [self assert:0 equals:mouseMovedCalls message:@"1.4 There should be no mouseMoved call"]; + [self assert:0 equals:cursorUpdateCalls message:@"1.4 There should be no cursorUpdate call"]; + + [self assert:viewA equals:involvedViewForMouseExited message:@"1.4 viewA should receive mouseExited"]; + + // 2. viewA with mouseEntered adding viewB inside it. Testing if viewB receive mouseEntered & cursorUpdate + + var viewA = [[CPTrackingAreaViewLiveAddition alloc] initWithFrame:CGRectMake(20, 20, 40, 40)]; + + [[window contentView] setSubviews:[CPArray arrayWithObject:viewA]]; + + var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; + + var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; + + [viewA addTrackingArea:viewATrackingArea]; + + // Step 2.1 : enter viewA (then add viewB thus enter also viewB) + + [self moveMouseAtPoint:CGPointMake(25, 25) dragging:NO]; + + [self assert:2 equals:mouseEnteredCalls message:@"2.1 There should be two mouseEntered calls"]; + [self assert:0 equals:mouseExitedCalls message:@"2.1 There should be no mouseExited call"]; + [self assert:0 equals:mouseMovedCalls message:@"2.1 There should be no mouseMoved call"]; + [self assert:2 equals:cursorUpdateCalls message:@"2.1 There should be two cursorUpdate calls"]; + + [self assert:[[viewA subviews] firstObject] equals:involvedViewForMouseEntered message:@"2.1 viewB should receive mouseEntered"]; + [self assert:[[viewA subviews] firstObject] equals:involvedViewForCursorUpdate message:@"2.1 viewB should receive cursorUpdate"]; + [self assert:[CPCursor crosshairCursor] equals:[CPCursor currentCursor] message:@"2.1 Final cursor should be crosshair cursor, determined by viewB"]; + + // Step 2.2 : exit viewA (thus also viewB) + + [self moveMouseAtPoint:CGPointMake(5, 5) dragging:NO]; + + [self assert:0 equals:mouseEnteredCalls message:@"2.2 There should be no mouseEntered calls"]; + [self assert:2 equals:mouseExitedCalls message:@"2.2 There should be two mouseExited call"]; + [self assert:0 equals:mouseMovedCalls message:@"2.2 There should be no mouseMoved call"]; + [self assert:0 equals:cursorUpdateCalls message:@"2.2 There should be no cursorUpdate calls"]; + + [self assert:[[viewA subviews] firstObject] equals:involvedViewForMouseExited message:@"2.2 viewB should receive mouseExited"]; + [self assert:[CPCursor arrowCursor] equals:[CPCursor currentCursor] message:@"2.2 Cursor should be an arrow"]; + + // 3. viewA with mouseEntered adding viewB inside it BUT with CPTrackingAssumeInside. Testing if viewB receive only cursorUpdate + + var viewA = [[CPTrackingAreaViewLiveAddition2 alloc] initWithFrame:CGRectMake(20, 20, 40, 40)]; + + [[window contentView] setSubviews:[CPArray arrayWithObject:viewA]]; + + var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; + + var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; + + [viewA addTrackingArea:viewATrackingArea]; + + // Step 3.1 : enter viewA (then add viewB thus enter also viewB) + + [self moveMouseAtPoint:CGPointMake(25, 25) dragging:NO]; + + [self assert:1 equals:mouseEnteredCalls message:@"3.1 There should be two mouseEntered calls"]; + [self assert:0 equals:mouseExitedCalls message:@"3.1 There should be no mouseExited call"]; + [self assert:0 equals:mouseMovedCalls message:@"3.1 There should be no mouseMoved call"]; + [self assert:2 equals:cursorUpdateCalls message:@"3.1 There should be two cursorUpdate calls"]; + + [self assert:viewA equals:involvedViewForMouseEntered message:@"3.1 viewB should receive mouseEntered"]; + [self assert:[[viewA subviews] firstObject] equals:involvedViewForCursorUpdate message:@"3.1 viewB should receive cursorUpdate"]; + [self assert:[CPCursor crosshairCursor] equals:[CPCursor currentCursor] message:@"3.1 Final cursor should be crosshair cursor, determined by viewB"]; + + // Step 3.2 : exit viewA (thus also viewB) + + [self moveMouseAtPoint:CGPointMake(5, 5) dragging:NO]; + + [self assert:0 equals:mouseEnteredCalls message:@"3.2 There should be no mouseEntered calls"]; + [self assert:2 equals:mouseExitedCalls message:@"3.2 There should be two mouseExited call"]; + [self assert:0 equals:mouseMovedCalls message:@"3.2 There should be no mouseMoved call"]; + [self assert:0 equals:cursorUpdateCalls message:@"3.2 There should be no cursorUpdate calls"]; + + [self assert:[[viewA subviews] firstObject] equals:involvedViewForMouseExited message:@"3.2 viewB should receive mouseExited"]; + [self assert:[CPCursor arrowCursor] equals:[CPCursor currentCursor] message:@"3.2 Cursor should be an arrow"]; +} + - (void)updateTrackingAreas { updateTrackingAreasCalls++; @@ -1472,6 +1610,77 @@ var updateTrackingAreasCalls, @end +@implementation CPTrackingAreaViewLiveRemoval : CPTrackingAreaView +{ + +} + +- (void)cursorUpdate:(CPEvent)anEvent +{ + [[CPCursor pointingHandCursor] set]; + + [super cursorUpdate:anEvent]; +} + +- (void)mouseEntered:(CPEvent)anEvent +{ + [self removeFromSuperview]; + + [super mouseEntered:anEvent]; +} + +@end + +@implementation CPTrackingAreaViewLiveAddition : CPTrackingAreaView +{ + +} + +- (void)cursorUpdate:(CPEvent)anEvent +{ + [[CPCursor pointingHandCursor] set]; + + [super cursorUpdate:anEvent]; +} + +- (void)mouseEntered:(CPEvent)anEvent +{ + var viewB = [[CPTrackingAreaViewWithCursorUpdate alloc] initWithFrame:CGRectMake(0, 0, 40, 40)]; + + [self addSubview:viewB]; + + [viewB addTrackingArea:[[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect owner:viewB userInfo:nil]]; + + [super mouseEntered:anEvent]; +} + +@end + +@implementation CPTrackingAreaViewLiveAddition2 : CPTrackingAreaView +{ + +} + +- (void)cursorUpdate:(CPEvent)anEvent +{ + [[CPCursor pointingHandCursor] set]; + + [super cursorUpdate:anEvent]; +} + +- (void)mouseEntered:(CPEvent)anEvent +{ + var viewB = [[CPTrackingAreaViewWithCursorUpdate alloc] initWithFrame:CGRectMake(0, 0, 40, 40)]; + + [self addSubview:viewB]; + + [viewB addTrackingArea:[[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect | CPTrackingAssumeInside owner:viewB userInfo:nil]]; + + [super mouseEntered:anEvent]; +} + +@end + @implementation CPLayoutView : CPView { From ad48381744ffc5d764c00623cb3a6dc4f222acfc Mon Sep 17 00:00:00 2001 From: Antoine Mercadal Date: Tue, 19 Jan 2016 13:26:10 -0800 Subject: [PATCH 08/16] FIXED: Bug introduced by https://github.com/cappuccino/cappuccino/commit/9fba72cbab42145dc585c01efd82ce645373c04e#diff-227ef108401c2160357ae3fc65018c65R620 The changes in CPBox were messing with the subviews encoding. This patch restores the previous behavior of CPBox. The original bug might be reintroduced, but this was definitly not the good solution --- AppKit/CPBox.j | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/AppKit/CPBox.j b/AppKit/CPBox.j index 948151f1c..29d0b3a08 100644 --- a/AppKit/CPBox.j +++ b/AppKit/CPBox.j @@ -598,8 +598,7 @@ var CPBoxTypeKey = @"CPBoxTypeKey", CPBoxBorderTypeKey = @"CPBoxBorderTypeKey", CPBoxTitle = @"CPBoxTitle", CPBoxTitlePosition = @"CPBoxTitlePosition", - CPBoxTitleView = @"CPBoxTitleView", - CPBoxContentView = @"CPBoxContentView"; + CPBoxTitleView = @"CPBoxTitleView"; @implementation CPBox (CPCoding) @@ -616,8 +615,7 @@ var CPBoxTypeKey = @"CPBoxTypeKey", _titlePosition = [aCoder decodeIntForKey:CPBoxTitlePosition]; _titleView = [aCoder decodeObjectForKey:CPBoxTitleView] || [CPTextField labelWithTitle:_title]; - _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; - [self replaceSubview:_contentView with:[self subviews][0]]; + _contentView = [self subviews][0]; [self setAutoresizesSubviews:YES]; [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; @@ -637,7 +635,6 @@ var CPBoxTypeKey = @"CPBoxTypeKey", [aCoder encodeObject:_title forKey:CPBoxTitle]; [aCoder encodeInt:_titlePosition forKey:CPBoxTitlePosition]; [aCoder encodeObject:_titleView forKey:CPBoxTitleView]; - [aCoder encodeObject:_contentView forKey:CPBoxContentView]; } @end From bf8070c42d64f3def6d918fa617f417155c3b092 Mon Sep 17 00:00:00 2001 From: Antoine Mercadal Date: Tue, 19 Jan 2016 16:08:59 -0800 Subject: [PATCH 09/16] Revert "FIXED: Bug introduced by https://github.com/cappuccino/cappuccino/commit/9fba72cbab42145dc585c01efd82ce645373c04e#diff-227ef108401c2160357ae3fc65018c65R620" This reverts commit ad48381744ffc5d764c00623cb3a6dc4f222acfc. --- AppKit/CPBox.j | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AppKit/CPBox.j b/AppKit/CPBox.j index 29d0b3a08..948151f1c 100644 --- a/AppKit/CPBox.j +++ b/AppKit/CPBox.j @@ -598,7 +598,8 @@ var CPBoxTypeKey = @"CPBoxTypeKey", CPBoxBorderTypeKey = @"CPBoxBorderTypeKey", CPBoxTitle = @"CPBoxTitle", CPBoxTitlePosition = @"CPBoxTitlePosition", - CPBoxTitleView = @"CPBoxTitleView"; + CPBoxTitleView = @"CPBoxTitleView", + CPBoxContentView = @"CPBoxContentView"; @implementation CPBox (CPCoding) @@ -615,7 +616,8 @@ var CPBoxTypeKey = @"CPBoxTypeKey", _titlePosition = [aCoder decodeIntForKey:CPBoxTitlePosition]; _titleView = [aCoder decodeObjectForKey:CPBoxTitleView] || [CPTextField labelWithTitle:_title]; - _contentView = [self subviews][0]; + _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; + [self replaceSubview:_contentView with:[self subviews][0]]; [self setAutoresizesSubviews:YES]; [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; @@ -635,6 +637,7 @@ var CPBoxTypeKey = @"CPBoxTypeKey", [aCoder encodeObject:_title forKey:CPBoxTitle]; [aCoder encodeInt:_titlePosition forKey:CPBoxTitlePosition]; [aCoder encodeObject:_titleView forKey:CPBoxTitleView]; + [aCoder encodeObject:_contentView forKey:CPBoxContentView]; } @end From 00bdbcf857d93c9276853d0ee181f9595facf6f7 Mon Sep 17 00:00:00 2001 From: Antoine Mercadal Date: Tue, 19 Jan 2016 16:10:31 -0800 Subject: [PATCH 10/16] FIXED: only set a content view of CPBox if it is not a separator --- AppKit/CPBox.j | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AppKit/CPBox.j b/AppKit/CPBox.j index 948151f1c..d36fb85e8 100644 --- a/AppKit/CPBox.j +++ b/AppKit/CPBox.j @@ -616,8 +616,11 @@ var CPBoxTypeKey = @"CPBoxTypeKey", _titlePosition = [aCoder decodeIntForKey:CPBoxTitlePosition]; _titleView = [aCoder decodeObjectForKey:CPBoxTitleView] || [CPTextField labelWithTitle:_title]; - _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; - [self replaceSubview:_contentView with:[self subviews][0]]; + if (_boxType != CPBoxSeparator) + { + _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; + [self replaceSubview:_contentView with:[self subviews][0]]; + } [self setAutoresizesSubviews:YES]; [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; From 4482089712a0c7dbbbd235093b4b50ca6ad3452e Mon Sep 17 00:00:00 2001 From: Didier Korthoudt Date: Wed, 20 Jan 2016 22:08:10 +0100 Subject: [PATCH 11/16] FIXED: corrected replaceSubview parameters order --- AppKit/CPBox.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPBox.j b/AppKit/CPBox.j index d36fb85e8..717229ad2 100644 --- a/AppKit/CPBox.j +++ b/AppKit/CPBox.j @@ -619,7 +619,7 @@ var CPBoxTypeKey = @"CPBoxTypeKey", if (_boxType != CPBoxSeparator) { _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; - [self replaceSubview:_contentView with:[self subviews][0]]; + [self replaceSubview:[self subviews][0] with:_contentView]; } [self setAutoresizesSubviews:YES]; From 3411c340abfd9485e964eb8eac96da9dd234dd3c Mon Sep 17 00:00:00 2001 From: Didier Korthoudt Date: Wed, 20 Jan 2016 23:50:22 +0100 Subject: [PATCH 12/16] FIXED: incorrect view hierarchy update --- AppKit/CPBox.j | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AppKit/CPBox.j b/AppKit/CPBox.j index 717229ad2..c26877f07 100644 --- a/AppKit/CPBox.j +++ b/AppKit/CPBox.j @@ -618,8 +618,10 @@ var CPBoxTypeKey = @"CPBoxTypeKey", if (_boxType != CPBoxSeparator) { - _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; - [self replaceSubview:[self subviews][0] with:_contentView]; + _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; + var malformedContentView = [self subviews][0]; + [_contentView setSubviews:[malformedContentView subviews]]; + [self replaceSubview:malformedContentView with:_contentView]; } [self setAutoresizesSubviews:YES]; From b3d8180541cb708fcca7c9f5ff7732ee5ecc3f68 Mon Sep 17 00:00:00 2001 From: Didier Korthoudt Date: Thu, 21 Jan 2016 00:10:38 +0100 Subject: [PATCH 13/16] FORMATTING: added comment to workaround --- AppKit/CPBox.j | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AppKit/CPBox.j b/AppKit/CPBox.j index c26877f07..9ab1d47d1 100644 --- a/AppKit/CPBox.j +++ b/AppKit/CPBox.j @@ -618,6 +618,13 @@ var CPBoxTypeKey = @"CPBoxTypeKey", if (_boxType != CPBoxSeparator) { + // FIXME: we have a problem with CIB decoding here. + // We should be able to simply add : _contentView = [self subviews][0] + // but first box subview seems to be malformed (badly decoded). + // For example, when deployed, this view doesn't have its _trackingAreas array initialized. + // As a (temporary) workaround, we encode/decode the _contentView property. We then transfer the subview hierarchy + // and replace the first (and only) box subview with this _contentView + _contentView = [aCoder decodeObjectForKey:CPBoxContentView] || [[CPView alloc] initWithFrame:[self bounds]]; var malformedContentView = [self subviews][0]; [_contentView setSubviews:[malformedContentView subviews]]; From 2877178b3469a696a03895ccd0531aeebf4a74d7 Mon Sep 17 00:00:00 2001 From: Didier Korthoudt Date: Thu, 21 Jan 2016 20:46:37 +0100 Subject: [PATCH 14/16] Formatting: following Aparajita's recommandations --- AppKit/CPWindow/CPWindow.j | 2 + Tests/AppKit/CPViewTest.j | 91 +++++++++++++------------------------- 2 files changed, 32 insertions(+), 61 deletions(-) diff --git a/AppKit/CPWindow/CPWindow.j b/AppKit/CPWindow/CPWindow.j index fad98a966..9c4880938 100644 --- a/AppKit/CPWindow/CPWindow.j +++ b/AppKit/CPWindow/CPWindow.j @@ -3877,10 +3877,12 @@ var interpolate = function(fromValue, toValue, progress) _restartHandlingTrackingAreaEvent = YES; if (assumeInside) + { if (_handlingTrackingAreaEvent) [_mouseEnteredStack addObject:trackingArea]; else [_previousMouseEnteredStack addObject:trackingArea]; + } } - (void)_purgeTrackingArea:(CPTrackingArea)trackingArea diff --git a/Tests/AppKit/CPViewTest.j b/Tests/AppKit/CPViewTest.j index 740601c4c..383db1a99 100644 --- a/Tests/AppKit/CPViewTest.j +++ b/Tests/AppKit/CPViewTest.j @@ -1127,34 +1127,32 @@ var updateTrackingAreasCalls, // Complex test for cursor update frontmost tracking area detection - var viewA = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(0, 30, 40, 40)]; - var viewB = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(30, 20, 40, 40)]; - var viewC = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(10, 0, 40, 40)]; + var viewA = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(0, 30, 40, 40)], + viewB = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(30, 20, 40, 40)], + viewC = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(10, 0, 40, 40)]; [contentView setSubviews:[CPArray array]]; [contentView addSubview:viewA]; [contentView addSubview:viewB]; [contentView addSubview:viewC]; - var subviewA = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(20, 0, 20, 20)]; - var subviewB = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; - var subviewC = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(0, 20, 40, 20)]; + var subviewA = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(20, 0, 20, 20)], + subviewB = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)], + subviewC = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(0, 20, 40, 20)]; [viewA addSubview:subviewA]; [viewB addSubview:subviewB]; [viewC addSubview:subviewC]; - var options = CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; - var options2 = CPTrackingMouseEnteredAndExited | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; - var options3 = CPTrackingCursorUpdate | CPTrackingActiveInActiveApp; - - var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; - var viewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewB userInfo:nil]; - var viewCTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options2 owner:viewC userInfo:nil]; - - var subviewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:subviewA userInfo:nil]; - var subviewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:subviewB userInfo:nil]; - var subviewCtrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMake(20, 0, 20, 20) options:options3 owner:subviewC userInfo:nil]; + var options = CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect, + options2 = CPTrackingMouseEnteredAndExited | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect, + options3 = CPTrackingCursorUpdate | CPTrackingActiveInActiveApp, + viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil], + viewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewB userInfo:nil], + viewCTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options2 owner:viewC userInfo:nil], + subviewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:subviewA userInfo:nil], + subviewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:subviewB userInfo:nil], + subviewCtrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMake(20, 0, 20, 20) options:options3 owner:subviewC userInfo:nil]; [viewA addTrackingArea:viewATrackingArea]; [viewB addTrackingArea:viewBTrackingArea]; @@ -1222,18 +1220,18 @@ var updateTrackingAreasCalls, // Cursor tests - var viewA = [[CPTrackingAreaViewWithCursorUpdate alloc] initWithFrame:CGRectMake(20, 20, 40, 40)]; - var viewB = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(10, 10, 80, 80)]; - var viewC = [[CPTrackingAreaViewWithoutCursorUpdate alloc] initWithFrame:CGRectMake(10, 10, 80, 80)]; + var viewA = [[CPTrackingAreaViewWithCursorUpdate alloc] initWithFrame:CGRectMake(20, 20, 40, 40)], + viewB = [[CPTrackingAreaView alloc] initWithFrame:CGRectMake(10, 10, 80, 80)], + viewC = [[CPTrackingAreaViewWithoutCursorUpdate alloc] initWithFrame:CGRectMake(10, 10, 80, 80)]; [contentView setSubviews:[CPArray array]]; [contentView addSubview:viewA]; var options = CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; - var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; - var viewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewB userInfo:nil]; - var viewCTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewC userInfo:nil]; + var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil], + viewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewB userInfo:nil], + viewCTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewC userInfo:nil]; [viewA addTrackingArea:viewATrackingArea]; [viewB addTrackingArea:viewBTrackingArea]; @@ -1364,16 +1362,15 @@ var updateTrackingAreasCalls, { // 1. viewB inside viewA with mouseEntered removing itself - var viewA = [[CPTrackingAreaViewWithCursorUpdate alloc] initWithFrame:CGRectMake(20, 20, 40, 40)]; - var viewB = [[CPTrackingAreaViewLiveRemoval alloc] initWithFrame:CGRectMake(10, 10, 20, 20)]; + var viewA = [[CPTrackingAreaViewWithCursorUpdate alloc] initWithFrame:CGRectMake(20, 20, 40, 40)] + viewB = [[CPTrackingAreaViewLiveRemoval alloc] initWithFrame:CGRectMake(10, 10, 20, 20)]; [[window contentView] setSubviews:[CPArray arrayWithObject:viewA]]; [viewA addSubview:viewB]; - var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; - - var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; - var viewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewB userInfo:nil]; + var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect, + viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil], + viewBTrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewB userInfo:nil]; [viewA addTrackingArea:viewATrackingArea]; [viewB addTrackingArea:viewBTrackingArea]; @@ -1429,9 +1426,8 @@ var updateTrackingAreasCalls, [[window contentView] setSubviews:[CPArray arrayWithObject:viewA]]; - var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; - - var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; + var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect, + viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; [viewA addTrackingArea:viewATrackingArea]; @@ -1466,9 +1462,8 @@ var updateTrackingAreasCalls, [[window contentView] setSubviews:[CPArray arrayWithObject:viewA]]; - var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect; - - var viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; + var options = CPTrackingMouseEnteredAndExited | CPTrackingCursorUpdate | CPTrackingActiveInActiveApp | CPTrackingInVisibleRect, + viewATrackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() options:options owner:viewA userInfo:nil]; [viewA addTrackingArea:viewATrackingArea]; @@ -1552,21 +1547,16 @@ var updateTrackingAreasCalls, @end @implementation CPTrackingAreaView : CPView -{ - -} - (void)mouseEntered:(CPEvent)anEvent { mouseEnteredCalls++; - involvedViewForMouseEntered = [[anEvent trackingArea] view]; } - (void)mouseExited:(CPEvent)anEvent { mouseExitedCalls++; - involvedViewForMouseExited = [[anEvent trackingArea] view]; } @@ -1578,7 +1568,6 @@ var updateTrackingAreasCalls, - (void)cursorUpdate:(CPEvent)anEvent { cursorUpdateCalls++; - involvedViewForCursorUpdate = [[anEvent trackingArea] view]; } @@ -1590,56 +1579,40 @@ var updateTrackingAreasCalls, @end @implementation CPTrackingAreaViewWithCursorUpdate : CPTrackingAreaView -{ - -} - (void)cursorUpdate:(CPEvent)anEvent { [[CPCursor crosshairCursor] set]; - [super cursorUpdate:anEvent]; } @end @implementation CPTrackingAreaViewWithoutCursorUpdate : CPView -{ - -} @end @implementation CPTrackingAreaViewLiveRemoval : CPTrackingAreaView -{ - -} - (void)cursorUpdate:(CPEvent)anEvent { [[CPCursor pointingHandCursor] set]; - [super cursorUpdate:anEvent]; } - (void)mouseEntered:(CPEvent)anEvent { [self removeFromSuperview]; - [super mouseEntered:anEvent]; } @end @implementation CPTrackingAreaViewLiveAddition : CPTrackingAreaView -{ - -} - (void)cursorUpdate:(CPEvent)anEvent { [[CPCursor pointingHandCursor] set]; - [super cursorUpdate:anEvent]; } @@ -1657,14 +1630,10 @@ var updateTrackingAreasCalls, @end @implementation CPTrackingAreaViewLiveAddition2 : CPTrackingAreaView -{ - -} - (void)cursorUpdate:(CPEvent)anEvent { [[CPCursor pointingHandCursor] set]; - [super cursorUpdate:anEvent]; } From 55d41fca0586b56bb7c1fe6e652c742d7682f55a Mon Sep 17 00:00:00 2001 From: Alexandre Wilhelm Date: Tue, 2 Feb 2016 13:05:01 -0800 Subject: [PATCH 15/16] New: added twitter account in the READMA --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index d8ffab704..7823d7da2 100644 --- a/README.markdown +++ b/README.markdown @@ -15,7 +15,7 @@ with the complexities of traditional web technologies like HTML, CSS, or even the DOM. The unpleasantries of building complex cross browser applications are abstracted away for you. -For more information, see . +For more information, see . Follow @cappuccino on Twitter for updates on the project. System Requirements ------------------- From 5e3fe139fca25f2698b046b72de37f2f7c990cb7 Mon Sep 17 00:00:00 2001 From: Alexandre Wilhelm Date: Tue, 2 Feb 2016 13:05:46 -0800 Subject: [PATCH 16/16] Fixed: @cappuccino is now a link in the README... --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 7823d7da2..111cdbd3d 100644 --- a/README.markdown +++ b/README.markdown @@ -15,7 +15,7 @@ with the complexities of traditional web technologies like HTML, CSS, or even the DOM. The unpleasantries of building complex cross browser applications are abstracted away for you. -For more information, see . Follow @cappuccino on Twitter for updates on the project. +For more information, see . Follow [@cappuccino](https://twitter.com/cappuccino) on Twitter for updates on the project. System Requirements -------------------