diff --git a/AppKit/CPBox.j b/AppKit/CPBox.j index 948151f1c..9ab1d47d1 100644 --- a/AppKit/CPBox.j +++ b/AppKit/CPBox.j @@ -616,8 +616,20 @@ 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) + { + // 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]]; + [self replaceSubview:malformedContentView with:_contentView]; + } [self setAutoresizesSubviews:YES]; [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; 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..9c4880938 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,143 @@ 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 +4013,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 +4036,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 +4074,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 +4267,7 @@ var interpolate = function(fromValue, toValue, progress) case CPMouseExited: [trackingOwner mouseExited:queuedEvent]; break; - + case CPCursorUpdate: [trackingOwner updateTrackingAreas]; diff --git a/Foundation/CPPredicate/CPExpression.j b/Foundation/CPPredicate/CPExpression.j index 06ed6b0ba..10ca2b598 100644 --- a/Foundation/CPPredicate/CPExpression.j +++ b/Foundation/CPPredicate/CPExpression.j @@ -28,3 +28,5 @@ @import "_CPAggregateExpression.j" @import "_CPSetExpression.j" @import "_CPSubqueryExpression.j" +@import "_CPBlockExpression.j" +@import "_CPConditionalExpression.j" diff --git a/Foundation/CPPredicate/_CPAggregateExpression.j b/Foundation/CPPredicate/_CPAggregateExpression.j index d3bdfc406..40760c626 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 @@ -49,11 +49,6 @@ return YES; } -- (id)collection -{ - return _aggregate; -} - - (id)expressionValueWithObject:(id)object context:(CPDictionary)context { return [_aggregate arrayByApplyingBlock:function(exp, _) diff --git a/Foundation/CPPredicate/_CPBlockExpression.j b/Foundation/CPPredicate/_CPBlockExpression.j new file mode 100644 index 000000000..299f250ee --- /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 @accessors(getter=expressionBlock); + CPArray _arguments @accessors(getter=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; +} + +- (id)expressionValueWithObject:(id)object context:(CPDictionary)context +{ + var args = []; + + [_arguments enumerateObjectsUsingBlock:function(exp, idx) + { + [args addObject:[exp expressionValueWithObject:object context:context]]; + }]; + + 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(function, %@)", [_arguments description]]; +} + +@end \ No newline at end of file diff --git a/Foundation/CPPredicate/_CPConditionalExpression.j b/Foundation/CPPredicate/_CPConditionalExpression.j new file mode 100644 index 000000000..8fc544580 --- /dev/null +++ b/Foundation/CPPredicate/_CPConditionalExpression.j @@ -0,0 +1,78 @@ +/* + * _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 @accessors(getter=predicate); + CPExpression _trueExpression @accessors(getter=trueExpression); + CPExpression _falseExpression @accessors(getter=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; +} + +- (id)expressionValueWithObject:(id)object context:(CPDictionary)context +{ + var eval = [_predicate evaluateWithObject:object substitutionVariables:context], + exp = eval ? _trueExpression : _falseExpression; + + return [exp expressionValueWithObject:object context:context]; +} + +- (CPExpression)_expressionWithSubstitutionVariables:(CPDictionary)bindings +{ + var predicate = [_predicate predicateWithSubstitutionVariables:bindings], + trueExp = [_trueExpression _expressionWithSubstitutionVariables:bindings], + falseExp = [_falseExpression _expressionWithSubstitutionVariables:bindings]; + + return [[_CPConditionalExpression alloc] initWithPredicate:predicate trueExpression:trueExp falseExpression:falseExp]; +} + +- (CPString)description +{ + return [CPString stringWithFormat:@"TERNARY(%@,%@,%@)", [_predicate predicateFormat], [_trueExpression description], [_falseExpression description]]; +} + +@end \ No newline at end of file diff --git a/Foundation/CPPredicate/_CPConstantValueExpression.j b/Foundation/CPPredicate/_CPConstantValueExpression.j index 397ce6371..1c1bbbce9 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 5a9561cb7..8a671c5b2 100644 --- a/Foundation/CPPredicate/_CPExpression.j +++ b/Foundation/CPPredicate/_CPExpression.j @@ -65,6 +65,14 @@ 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; +/*! + An expression that returns an expression that depends on the evaluation of a predicate. +*/ +CPConditionalExpressionType = 11; /*! @ingroup foundation @@ -259,6 +267,32 @@ 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]; +} + ++ (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. @@ -316,7 +350,7 @@ CPMinusSetExpressionType = 9; /*! 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 @@ -337,8 +371,8 @@ CPMinusSetExpressionType = 9; } /*! - 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 @@ -380,6 +414,39 @@ CPMinusSetExpressionType = 9; 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 b0d854dd9..1630bc007 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/_CPPredicate.j b/Foundation/CPPredicate/_CPPredicate.j index 56bfcd643..34001c415 100644 --- a/Foundation/CPPredicate/_CPPredicate.j +++ b/Foundation/CPPredicate/_CPPredicate.j @@ -744,16 +744,43 @@ 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]) + 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 = [self parsePredicate], + trueExpression, + falseExpression; + + 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 [[_CPSubqueryExpression alloc] initWithExpression:collection usingIteratorExpression:variableExpression predicate:subpredicate]; + return [CPExpression expressionForConditional:predicate trueExpression:trueExpression falseExpression:falseExpression]; } if ([self scanString:@"FUNCTION" intoString:NULL]) 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]; diff --git a/README.markdown b/README.markdown index d8ffab704..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 . +For more information, see . Follow [@cappuccino](https://twitter.com/cappuccino) on Twitter for updates on the project. System Requirements ------------------- diff --git a/Tests/AppKit/CPViewTest.j b/Tests/AppKit/CPViewTest.j index 426a6c086..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]; @@ -1360,6 +1358,141 @@ var updateTrackingAreasCalls, } +- (void)testTrackingAreasLiveViewHierarchyModification +{ + // 1. viewB inside viewA with mouseEntered removing itself + + 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, + 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]; + + // 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, + 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, + 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++; @@ -1414,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]; } @@ -1440,7 +1568,6 @@ var updateTrackingAreasCalls, - (void)cursorUpdate:(CPEvent)anEvent { cursorUpdateCalls++; - involvedViewForCursorUpdate = [[anEvent trackingArea] view]; } @@ -1452,22 +1579,73 @@ 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]; +} + +- (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 diff --git a/Tests/Foundation/CPPredicateTest.j b/Tests/Foundation/CPPredicateTest.j index ff89760c0..3a9840e96 100644 --- a/Tests/Foundation/CPPredicateTest.j +++ b/Tests/Foundation/CPPredicateTest.j @@ -79,6 +79,17 @@ 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"]; + + 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 @@ -147,6 +158,27 @@ [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)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]; @@ -381,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 @@ -439,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 @@ -490,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];