/* * Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "WebViewInternal.h" #import "WebFrameInternal.h" #import "WebTextCompletionController.h" #import "WebViewData.h" #import #import #import #import #import @class NSTextInputContext; @interface NSResponder (WebNSResponderDetails) - (NSTextInputContext *)inputContext; @end #if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) @interface NSObject (NSTextInputContextDetails) - (BOOL)wantsToHandleMouseEvents; - (BOOL)handleMouseEvent:(NSEvent *)event; @end #endif // Any non-zero value will do, but using something recognizable might help us debug some day. #define TRACKING_RECT_TAG 0xBADFACE using namespace WebCore; @implementation WebView (WebViewEventHandling) static WebView *lastMouseoverView; - (void)_closingEventHandling { if (lastMouseoverView == self) lastMouseoverView = nil; } - (NSView *)hitTest:(NSPoint)point { if ([[self superview] mouse:point inRect:[self frame]]) return self; return nil; } - (void)_setMouseDownEvent:(NSEvent *)event { ASSERT(!event || [event type] == NSLeftMouseDown || [event type] == NSRightMouseDown || [event type] == NSOtherMouseDown); if (event == _private->mouseDownEvent) return; [event retain]; [_private->mouseDownEvent release]; _private->mouseDownEvent = event; } - (void)mouseDown:(NSEvent *)event { // FIXME (Viewless): This method should be shared with WebHTMLView, which needs to // do the same work in the usesDocumentViews case. We don't want to maintain two // duplicate copies of this method. // There's a chance that responding to this event will run a nested event loop, and // fetching a new event might release the old one. Retaining and then autoreleasing // the current event prevents that from causing a problem inside WebKit or AppKit code. [[event retain] autorelease]; RetainPtr protector = self; if ([[self inputContext] wantsToHandleMouseEvents] && [[self inputContext] handleMouseEvent:event]) return; _private->handlingMouseDownEvent = YES; // Record the mouse down position so we can determine drag hysteresis. [self _setMouseDownEvent:event]; NSInputManager *currentInputManager = [NSInputManager currentInputManager]; if ([currentInputManager wantsToHandleMouseEvents] && [currentInputManager handleMouseEvent:event]) goto done; [_private->completionController endRevertingChange:NO moveLeft:NO]; // If the web page handles the context menu event and menuForEvent: returns nil, we'll get control click events here. // We don't want to pass them along to KHTML a second time. if (!([event modifierFlags] & NSControlKeyMask)) { _private->ignoringMouseDraggedEvents = NO; // Don't do any mouseover while the mouse is down. [self _cancelUpdateMouseoverTimer]; // Let WebCore get a chance to deal with the event. This will call back to us // to start the autoscroll timer if appropriate. if (Frame* frame = [self _mainCoreFrame]) frame->eventHandler()->mouseDown(event); } done: _private->handlingMouseDownEvent = NO; } - (void)mouseUp:(NSEvent *)event { // FIXME (Viewless): This method should be shared with WebHTMLView, which needs to // do the same work in the usesDocumentViews case. We don't want to maintain two // duplicate copies of this method. // There's a chance that responding to this event will run a nested event loop, and // fetching a new event might release the old one. Retaining and then autoreleasing // the current event prevents that from causing a problem inside WebKit or AppKit code. [[event retain] autorelease]; [self _setMouseDownEvent:nil]; NSInputManager *currentInputManager = [NSInputManager currentInputManager]; if ([currentInputManager wantsToHandleMouseEvents] && [currentInputManager handleMouseEvent:event]) return; [self retain]; [self _stopAutoscrollTimer]; if (Frame* frame = [self _mainCoreFrame]) frame->eventHandler()->mouseUp(event); [self _updateMouseoverWithFakeEvent]; [self release]; } - (void)mouseDragged:(NSEvent *)event { // There's a chance that responding to this event will run a nested event loop, and // fetching a new event might release the old one. Retaining and then autoreleasing // the current event prevents that from causing a problem inside WebKit or AppKit code. [[event retain] autorelease]; NSInputManager *currentInputManager = [NSInputManager currentInputManager]; if ([currentInputManager wantsToHandleMouseEvents] && [currentInputManager handleMouseEvent:event]) return; [self retain]; if (!_private->ignoringMouseDraggedEvents) { if (Frame* frame = core([self mainFrame])) frame->eventHandler()->mouseDragged(event); } [self release]; } - (void)otherMouseDown:(NSEvent *)event { if ([event buttonNumber] == 2) [self mouseDown:event]; else [super otherMouseDown:event]; } - (void)otherMouseDragged:(NSEvent *)event { if ([event buttonNumber] == 2) [self mouseDragged:event]; else [super otherMouseDragged:event]; } - (void)otherMouseUp:(NSEvent *)event { if ([event buttonNumber] == 2) [self mouseUp:event]; else [super otherMouseUp:event]; } + (void)_updateMouseoverWithEvent:(NSEvent *)event { WebView *oldView = lastMouseoverView; lastMouseoverView = nil; NSView *contentView = [[event window] contentView]; NSPoint locationForHitTest = [[contentView superview] convertPoint:[event locationInWindow] fromView:nil]; for (NSView *hitView = [contentView hitTest:locationForHitTest]; hitView; hitView = [hitView superview]) { if ([hitView isKindOfClass:[WebView class]]) { lastMouseoverView = static_cast(hitView); break; } } if (lastMouseoverView && lastMouseoverView->_private->hoverFeedbackSuspended) lastMouseoverView = nil; if (lastMouseoverView != oldView) { if (Frame* oldCoreFrame = [oldView _mainCoreFrame]) { NSEvent *oldViewEvent = [NSEvent mouseEventWithType:NSMouseMoved location:NSMakePoint(-1, -1) modifierFlags:[[NSApp currentEvent] modifierFlags] timestamp:[NSDate timeIntervalSinceReferenceDate] windowNumber:[[oldView window] windowNumber] context:[[NSApp currentEvent] context] eventNumber:0 clickCount:0 pressure:0]; oldCoreFrame->eventHandler()->mouseMoved(oldViewEvent); } } if (!lastMouseoverView) return; if (Frame* coreFrame = core([lastMouseoverView mainFrame])) coreFrame->eventHandler()->mouseMoved(event); } - (void)_updateMouseoverWithFakeEvent { [self _cancelUpdateMouseoverTimer]; NSEvent *fakeEvent = [NSEvent mouseEventWithType:NSMouseMoved location:[[self window] convertScreenToBase:[NSEvent mouseLocation]] modifierFlags:[[NSApp currentEvent] modifierFlags] timestamp:[NSDate timeIntervalSinceReferenceDate] windowNumber:[[self window] windowNumber] context:[[NSApp currentEvent] context] eventNumber:0 clickCount:0 pressure:0]; [[self class] _updateMouseoverWithEvent:fakeEvent]; } - (NSEvent *)_mouseDownEvent { return _private->mouseDownEvent; } - (void)_mouseMovedNotification:(NSNotification *)notification { [[self class] _updateMouseoverWithEvent:[[notification userInfo] objectForKey:@"NSEvent"]]; } - (void)_removeMouseMovedObserverUnconditionally { if (!_private || !_private->observingMouseMovedNotifications) return; [[NSNotificationCenter defaultCenter] removeObserver:self name:WKMouseMovedNotification() object:nil]; _private->observingMouseMovedNotifications = false; } - (void)_addMouseMovedObserver { if (_private->observingMouseMovedNotifications) return; // Unless the Dashboard asks us to do this for all windows, keep an observer going only for the key window. if (![[self window] isKeyWindow]) { #if ENABLE(DASHBOARD_SUPPORT) if (![self _dashboardBehavior:WebDashboardBehaviorAlwaysSendMouseEventsToAllWindows]) #endif return; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_mouseMovedNotification:) name:WKMouseMovedNotification() object:nil]; _private->observingMouseMovedNotifications = true; } - (void)_removeMouseMovedObserver { #if ENABLE(DASHBOARD_SUPPORT) // Don't remove the observer if we're running the Dashboard. if ([self _dashboardBehavior:WebDashboardBehaviorAlwaysSendMouseEventsToAllWindows]) return; #endif [self _mouseDidMoveOverElement:nil modifierFlags:0]; [self _removeMouseMovedObserverUnconditionally]; } - (void)_cancelUpdateMouseoverTimer { if (_private->updateMouseoverTimer) { CFRunLoopTimerInvalidate(_private->updateMouseoverTimer); CFRelease(_private->updateMouseoverTimer); _private->updateMouseoverTimer = NULL; } } - (void)_stopAutoscrollTimer { NSTimer *timer = _private->autoscrollTimer; _private->autoscrollTimer = nil; [_private->autoscrollTriggerEvent release]; _private->autoscrollTriggerEvent = nil; [timer invalidate]; [timer release]; } - (void)_sendToolTipMouseExited { // Nothing matters except window, trackingNumber, and userData. NSEvent *fakeEvent = [NSEvent enterExitEventWithType:NSMouseExited location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:[[self window] windowNumber] context:NULL eventNumber:0 trackingNumber:TRACKING_RECT_TAG userData:_private->trackingRectUserData]; [_private->trackingRectOwner mouseExited:fakeEvent]; } - (void)_sendToolTipMouseEntered { // Nothing matters except window, trackingNumber, and userData. NSEvent *fakeEvent = [NSEvent enterExitEventWithType:NSMouseEntered location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:[[self window] windowNumber] context:NULL eventNumber:0 trackingNumber:TRACKING_RECT_TAG userData:_private->trackingRectUserData]; [_private->trackingRectOwner mouseEntered:fakeEvent]; } - (void)_setToolTip:(NSString *)string { NSString *toolTip = [string length] == 0 ? nil : string; NSString *oldToolTip = _private->toolTip; if ((toolTip == nil || oldToolTip == nil) ? toolTip == oldToolTip : [toolTip isEqualToString:oldToolTip]) { return; } if (oldToolTip) { [self _sendToolTipMouseExited]; [oldToolTip release]; } _private->toolTip = [toolTip copy]; if (toolTip) { // See radar 3500217 for why we remove all tooltips rather than just the single one we created. [self removeAllToolTips]; NSRect wideOpenRect = NSMakeRect(-100000, -100000, 200000, 200000); _private->lastToolTipTag = [self addToolTipRect:wideOpenRect owner:self userData:NULL]; [self _sendToolTipMouseEntered]; } } // Utility function to make sure we don't return anything through the NSTextInput // API when an editable region is not currently focused. static BOOL isTextInput(Frame* coreFrame) { return coreFrame && !coreFrame->selection()->isNone() && coreFrame->selection()->isContentEditable(); } static BOOL isInPasswordField(Frame* coreFrame) { return coreFrame && coreFrame->selection()->isInPasswordField(); } - (void)_updateSelectionForInputManager { Frame* coreFrame = core([self mainFrame]); if (!coreFrame) return; BOOL exposeInputContext = isTextInput(coreFrame) && !isInPasswordField(coreFrame); if (exposeInputContext != _private->exposeInputContext) { _private->exposeInputContext = exposeInputContext; // Let AppKit cache a potentially changed input context. // WebCore routinely sets the selection to None when editing, and IMs become unhappy when an input context suddenly turns nil, see bug 26009. if (!coreFrame->selection()->isNone()) [NSApp updateWindows]; } if (!coreFrame->editor()->hasComposition()) return; if (coreFrame->editor()->ignoreCompositionSelectionChange()) return; unsigned start; unsigned end; if (coreFrame->editor()->getCompositionSelection(start, end)) [[NSInputManager currentInputManager] markedTextSelectionChanged:NSMakeRange(start, end - start) client:self]; else { coreFrame->editor()->confirmCompositionWithoutDisturbingSelection(); [[NSInputManager currentInputManager] markedTextAbandoned:self]; } } - (BOOL)_canEdit { Frame* coreFrame = core([self mainFrame]); return coreFrame && coreFrame->editor()->canEdit(); } - (BOOL)_hasSelection { Frame* coreFrame = core([self mainFrame]); return coreFrame && coreFrame->selection()->isRange(); } - (void)_updateFontPanel { // FIXME: NSTextView bails out if becoming or resigning first responder, for which it has ivar flags. Not // sure if we need to do something similar. if (![self _canEdit]) return; NSWindow *window = [self window]; // FIXME: is this first-responder check correct? What happens if a subframe is editable and is first responder? if ([NSApp keyWindow] != window || [window firstResponder] != self) return; bool multipleFonts = false; NSFont *font = nil; if (Frame* coreFrame = core([self mainFrame])) { if (const SimpleFontData* fd = coreFrame->editor()->fontForSelection(multipleFonts)) font = fd->getNSFont(); } // FIXME: for now, return a bogus font that distinguishes the empty selection from the non-empty // selection. We should be able to remove this once the rest of this code works properly. if (font == nil) font = [self _hasSelection] ? [NSFont menuFontOfSize:23] : [NSFont toolTipsFontOfSize:17]; ASSERT(font != nil); [[NSFontManager sharedFontManager] setSelectedFont:font isMultiple:multipleFonts]; // FIXME: we don't keep track of selected attributes, or set them on the font panel. This // appears to have no effect on the UI. E.g., underlined text in Mail or TextEdit is // not reflected in the font panel. Maybe someday this will change. } - (void)_selectionChanged { [self _updateSelectionForInputManager]; [self _updateFontPanel]; if (Frame* coreFrame = core([self mainFrame])) coreFrame->editor()->setStartNewKillRingSequence(true); } // Deliver mouseup events to the DOM for button 2. - (void)rightMouseUp:(NSEvent *)event { // There's a chance that if we run a nested event loop the event will be released. // Retaining and then autoreleasing prevents that from causing a problem later here or // inside AppKit code. [[event retain] autorelease]; [super rightMouseUp:event]; if (Frame* coreframe = core([self mainFrame])) coreframe->eventHandler()->mouseUp(event); } - (NSMenu *)menuForEvent:(NSEvent *)event { // There's a chance that if we run a nested event loop the event will be released. // Retaining and then autoreleasing prevents that from causing a problem later here or // inside AppKit code. [[event retain] autorelease]; [_private->completionController endRevertingChange:NO moveLeft:NO]; RefPtr coreFrame = core([self mainFrame]); if (!coreFrame) return nil; Page* page = coreFrame->page(); if (!page) return nil; // Match behavior of other browsers by sending a mousedown event for right clicks. _private->handlingMouseDownEvent = YES; page->contextMenuController()->clearContextMenu(); coreFrame->eventHandler()->mouseDown(event); BOOL handledEvent = coreFrame->eventHandler()->sendContextMenuEvent(event); _private->handlingMouseDownEvent = NO; if (!handledEvent) return nil; // Re-get page, since it might have gone away during event handling. page = coreFrame->page(); if (!page) return nil; ContextMenu* coreMenu = page->contextMenuController()->contextMenu(); if (!coreMenu) return nil; NSArray *menuItems = coreMenu->platformDescription(); if (!menuItems) return nil; NSUInteger count = [menuItems count]; if (!count) return nil; NSMenu* menu = [[[NSMenu alloc] init] autorelease]; for (NSUInteger i = 0; i < count; i++) [menu addItem:[menuItems objectAtIndex:i]]; return menu; } @end