FIXED: CPTrackingArea incorrect handling of view hierarchy updates while processing events

This commit is contained in:
Didier Korthoudt
2016-01-19 07:07:45 +01:00
parent a7b129147f
commit 0f30d7744d
3 changed files with 463 additions and 170 deletions
+20 -20
View File
@@ -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 views 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];
+234 -150
View File
@@ -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];
+209
View File
@@ -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
{