1 /* 2 * Copyright (C) 2006, 2007 Apple Inc. All rights reserved. 3 * Copyright (C) 2008 Nuanti Ltd. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY 15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR 18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27 #include "config.h" 28 #include "FocusController.h" 29 30 #include "AXObjectCache.h" 31 #include "Chrome.h" 32 #include "Document.h" 33 #include "Editor.h" 34 #include "EditorClient.h" 35 #include "Element.h" 36 #include "Event.h" 37 #include "EventHandler.h" 38 #include "EventNames.h" 39 #include "ExceptionCode.h" 40 #include "Frame.h" 41 #include "FrameTree.h" 42 #include "FrameView.h" 43 #include "HTMLAreaElement.h" 44 #include "HTMLImageElement.h" 45 #include "HTMLNames.h" 46 #include "HitTestResult.h" 47 #include "KeyboardEvent.h" 48 #include "Page.h" 49 #include "Range.h" 50 #include "RenderLayer.h" 51 #include "RenderObject.h" 52 #include "RenderWidget.h" 53 #include "ScrollAnimator.h" 54 #include "SelectionController.h" 55 #include "Settings.h" 56 #include "SpatialNavigation.h" 57 #include "Widget.h" 58 #include "htmlediting.h" // For firstPositionInOrBeforeNode 59 60 namespace WebCore { 61 62 using namespace HTMLNames; 63 using namespace std; 64 65 static inline void dispatchEventsOnWindowAndFocusedNode(Document* document, bool focused) 66 { 67 // If we have a focused node we should dispatch blur on it before we blur the window. 68 // If we have a focused node we should dispatch focus on it after we focus the window. 69 // https://bugs.webkit.org/show_bug.cgi?id=27105 70 71 // Do not fire events while modal dialogs are up. See https://bugs.webkit.org/show_bug.cgi?id=33962 72 if (Page* page = document->page()) { 73 if (page->defersLoading()) 74 return; 75 } 76 77 if (!focused && document->focusedNode()) 78 document->focusedNode()->dispatchBlurEvent(); 79 document->dispatchWindowEvent(Event::create(focused ? eventNames().focusEvent : eventNames().blurEvent, false, false)); 80 if (focused && document->focusedNode()) 81 document->focusedNode()->dispatchFocusEvent(); 82 } 83 84 FocusController::FocusController(Page* page) 85 : m_page(page) 86 , m_isActive(false) 87 , m_isFocused(false) 88 , m_isChangingFocusedFrame(false) 89 { 90 } 91 92 void FocusController::setFocusedFrame(PassRefPtr<Frame> frame) 93 { 94 ASSERT(!frame || frame->page() == m_page); 95 if (m_focusedFrame == frame || m_isChangingFocusedFrame) 96 return; 97 98 m_isChangingFocusedFrame = true; 99 100 RefPtr<Frame> oldFrame = m_focusedFrame; 101 RefPtr<Frame> newFrame = frame; 102 103 m_focusedFrame = newFrame; 104 105 // Now that the frame is updated, fire events and update the selection focused states of both frames. 106 if (oldFrame && oldFrame->view()) { 107 oldFrame->selection()->setFocused(false); 108 oldFrame->document()->dispatchWindowEvent(Event::create(eventNames().blurEvent, false, false)); 109 } 110 111 if (newFrame && newFrame->view() && isFocused()) { 112 newFrame->selection()->setFocused(true); 113 newFrame->document()->dispatchWindowEvent(Event::create(eventNames().focusEvent, false, false)); 114 } 115 116 m_page->chrome()->focusedFrameChanged(newFrame.get()); 117 118 m_isChangingFocusedFrame = false; 119 } 120 121 Frame* FocusController::focusedOrMainFrame() const 122 { 123 if (Frame* frame = focusedFrame()) 124 return frame; 125 return m_page->mainFrame(); 126 } 127 128 void FocusController::setFocused(bool focused) 129 { 130 if (isFocused() == focused) 131 return; 132 133 m_isFocused = focused; 134 135 if (!m_isFocused) 136 focusedOrMainFrame()->eventHandler()->stopAutoscrollTimer(); 137 138 if (!m_focusedFrame) 139 setFocusedFrame(m_page->mainFrame()); 140 141 if (m_focusedFrame->view()) { 142 m_focusedFrame->selection()->setFocused(focused); 143 dispatchEventsOnWindowAndFocusedNode(m_focusedFrame->document(), focused); 144 } 145 } 146 147 static Node* deepFocusableNode(FocusDirection direction, Node* node, KeyboardEvent* event) 148 { 149 // The node we found might be a HTMLFrameOwnerElement, so descend down the frame tree until we find either: 150 // 1) a focusable node, or 151 // 2) the deepest-nested HTMLFrameOwnerElement 152 while (node && node->isFrameOwnerElement()) { 153 HTMLFrameOwnerElement* owner = static_cast<HTMLFrameOwnerElement*>(node); 154 if (!owner->contentFrame()) 155 break; 156 157 Document* document = owner->contentFrame()->document(); 158 159 node = (direction == FocusDirectionForward) 160 ? document->nextFocusableNode(0, event) 161 : document->previousFocusableNode(0, event); 162 if (!node) { 163 node = owner; 164 break; 165 } 166 } 167 168 return node; 169 } 170 171 bool FocusController::setInitialFocus(FocusDirection direction, KeyboardEvent* event) 172 { 173 bool didAdvanceFocus = advanceFocus(direction, event, true); 174 175 // If focus is being set initially, accessibility needs to be informed that system focus has moved 176 // into the web area again, even if focus did not change within WebCore. PostNotification is called instead 177 // of handleFocusedUIElementChanged, because this will send the notification even if the element is the same. 178 if (AXObjectCache::accessibilityEnabled()) 179 focusedOrMainFrame()->document()->axObjectCache()->postNotification(focusedOrMainFrame()->document()->renderer(), AXObjectCache::AXFocusedUIElementChanged, true); 180 181 return didAdvanceFocus; 182 } 183 184 bool FocusController::advanceFocus(FocusDirection direction, KeyboardEvent* event, bool initialFocus) 185 { 186 switch (direction) { 187 case FocusDirectionForward: 188 case FocusDirectionBackward: 189 return advanceFocusInDocumentOrder(direction, event, initialFocus); 190 case FocusDirectionLeft: 191 case FocusDirectionRight: 192 case FocusDirectionUp: 193 case FocusDirectionDown: 194 return advanceFocusDirectionally(direction, event); 195 default: 196 ASSERT_NOT_REACHED(); 197 } 198 199 return false; 200 } 201 202 bool FocusController::advanceFocusInDocumentOrder(FocusDirection direction, KeyboardEvent* event, bool initialFocus) 203 { 204 Frame* frame = focusedOrMainFrame(); 205 ASSERT(frame); 206 Document* document = frame->document(); 207 208 Node* currentNode = document->focusedNode(); 209 // FIXME: Not quite correct when it comes to focus transitions leaving/entering the WebView itself 210 bool caretBrowsing = focusedOrMainFrame()->settings()->caretBrowsingEnabled(); 211 212 if (caretBrowsing && !currentNode) 213 currentNode = frame->selection()->start().deprecatedNode(); 214 215 document->updateLayoutIgnorePendingStylesheets(); 216 217 Node* node = (direction == FocusDirectionForward) 218 ? document->nextFocusableNode(currentNode, event) 219 : document->previousFocusableNode(currentNode, event); 220 221 // If there's no focusable node to advance to, move up the frame tree until we find one. 222 while (!node && frame) { 223 Frame* parentFrame = frame->tree()->parent(); 224 if (!parentFrame) 225 break; 226 227 Document* parentDocument = parentFrame->document(); 228 229 HTMLFrameOwnerElement* owner = frame->ownerElement(); 230 if (!owner) 231 break; 232 233 node = (direction == FocusDirectionForward) 234 ? parentDocument->nextFocusableNode(owner, event) 235 : parentDocument->previousFocusableNode(owner, event); 236 237 frame = parentFrame; 238 } 239 240 node = deepFocusableNode(direction, node, event); 241 242 if (!node) { 243 // We didn't find a node to focus, so we should try to pass focus to Chrome. 244 if (!initialFocus && m_page->chrome()->canTakeFocus(direction)) { 245 document->setFocusedNode(0); 246 setFocusedFrame(0); 247 m_page->chrome()->takeFocus(direction); 248 return true; 249 } 250 251 // Chrome doesn't want focus, so we should wrap focus. 252 Document* d = m_page->mainFrame()->document(); 253 node = (direction == FocusDirectionForward) 254 ? d->nextFocusableNode(0, event) 255 : d->previousFocusableNode(0, event); 256 257 node = deepFocusableNode(direction, node, event); 258 259 if (!node) 260 return false; 261 } 262 263 ASSERT(node); 264 265 if (node == document->focusedNode()) 266 // Focus wrapped around to the same node. 267 return true; 268 269 if (!node->isElementNode()) 270 // FIXME: May need a way to focus a document here. 271 return false; 272 273 if (node->isFrameOwnerElement()) { 274 // We focus frames rather than frame owners. 275 // FIXME: We should not focus frames that have no scrollbars, as focusing them isn't useful to the user. 276 HTMLFrameOwnerElement* owner = static_cast<HTMLFrameOwnerElement*>(node); 277 if (!owner->contentFrame()) 278 return false; 279 280 document->setFocusedNode(0); 281 setFocusedFrame(owner->contentFrame()); 282 return true; 283 } 284 285 // FIXME: It would be nice to just be able to call setFocusedNode(node) here, but we can't do 286 // that because some elements (e.g. HTMLInputElement and HTMLTextAreaElement) do extra work in 287 // their focus() methods. 288 289 Document* newDocument = node->document(); 290 291 if (newDocument != document) 292 // Focus is going away from this document, so clear the focused node. 293 document->setFocusedNode(0); 294 295 if (newDocument) 296 setFocusedFrame(newDocument->frame()); 297 298 if (caretBrowsing) { 299 Position position = firstPositionInOrBeforeNode(node); 300 VisibleSelection newSelection(position, position, DOWNSTREAM); 301 if (frame->selection()->shouldChangeSelection(newSelection)) 302 frame->selection()->setSelection(newSelection); 303 } 304 305 static_cast<Element*>(node)->focus(false); 306 return true; 307 } 308 309 static bool relinquishesEditingFocus(Node *node) 310 { 311 ASSERT(node); 312 ASSERT(node->rendererIsEditable()); 313 314 Node* root = node->rootEditableElement(); 315 Frame* frame = node->document()->frame(); 316 if (!frame || !root) 317 return false; 318 319 return frame->editor()->shouldEndEditing(rangeOfContents(root).get()); 320 } 321 322 static void clearSelectionIfNeeded(Frame* oldFocusedFrame, Frame* newFocusedFrame, Node* newFocusedNode) 323 { 324 if (!oldFocusedFrame || !newFocusedFrame) 325 return; 326 327 if (oldFocusedFrame->document() != newFocusedFrame->document()) 328 return; 329 330 SelectionController* s = oldFocusedFrame->selection(); 331 if (s->isNone()) 332 return; 333 334 bool caretBrowsing = oldFocusedFrame->settings()->caretBrowsingEnabled(); 335 if (caretBrowsing) 336 return; 337 338 Node* selectionStartNode = s->selection().start().deprecatedNode(); 339 if (selectionStartNode == newFocusedNode || selectionStartNode->isDescendantOf(newFocusedNode) || selectionStartNode->shadowAncestorNode() == newFocusedNode) 340 return; 341 342 if (Node* mousePressNode = newFocusedFrame->eventHandler()->mousePressNode()) { 343 if (mousePressNode->renderer() && !mousePressNode->canStartSelection()) { 344 // Don't clear the selection for contentEditable elements, but do clear it for input and textarea. See bug 38696. 345 Node * root = s->rootEditableElement(); 346 if (!root) 347 return; 348 349 if (Node* shadowAncestorNode = root->shadowAncestorNode()) { 350 if (!shadowAncestorNode->hasTagName(inputTag) && !shadowAncestorNode->hasTagName(textareaTag)) 351 return; 352 } 353 } 354 } 355 356 s->clear(); 357 } 358 359 bool FocusController::setFocusedNode(Node* node, PassRefPtr<Frame> newFocusedFrame) 360 { 361 RefPtr<Frame> oldFocusedFrame = focusedFrame(); 362 RefPtr<Document> oldDocument = oldFocusedFrame ? oldFocusedFrame->document() : 0; 363 364 Node* oldFocusedNode = oldDocument ? oldDocument->focusedNode() : 0; 365 if (oldFocusedNode == node) 366 return true; 367 368 // FIXME: Might want to disable this check for caretBrowsing 369 if (oldFocusedNode && oldFocusedNode->rootEditableElement() == oldFocusedNode && !relinquishesEditingFocus(oldFocusedNode)) 370 return false; 371 372 m_page->editorClient()->willSetInputMethodState(); 373 374 clearSelectionIfNeeded(oldFocusedFrame.get(), newFocusedFrame.get(), node); 375 376 if (!node) { 377 if (oldDocument) 378 oldDocument->setFocusedNode(0); 379 m_page->editorClient()->setInputMethodState(false); 380 return true; 381 } 382 383 RefPtr<Document> newDocument = node->document(); 384 385 if (newDocument && newDocument->focusedNode() == node) { 386 m_page->editorClient()->setInputMethodState(node->shouldUseInputMethod()); 387 return true; 388 } 389 390 if (oldDocument && oldDocument != newDocument) 391 oldDocument->setFocusedNode(0); 392 393 setFocusedFrame(newFocusedFrame); 394 395 // Setting the focused node can result in losing our last reft to node when JS event handlers fire. 396 RefPtr<Node> protect = node; 397 if (newDocument) { 398 bool successfullyFocused = newDocument->setFocusedNode(node); 399 if (!successfullyFocused) 400 return false; 401 } 402 403 if (newDocument->focusedNode() == node) 404 m_page->editorClient()->setInputMethodState(node->shouldUseInputMethod()); 405 406 return true; 407 } 408 409 void FocusController::setActive(bool active) 410 { 411 if (m_isActive == active) 412 return; 413 414 m_isActive = active; 415 416 if (FrameView* view = m_page->mainFrame()->view()) { 417 if (!view->platformWidget()) { 418 view->updateLayoutAndStyleIfNeededRecursive(); 419 view->updateControlTints(); 420 } 421 422 if (const HashSet<ScrollableArea*>* scrollableAreas = m_page->scrollableAreaSet()) { 423 HashSet<ScrollableArea*>::const_iterator end = scrollableAreas->end(); 424 for (HashSet<ScrollableArea*>::const_iterator it = scrollableAreas->begin(); it != end; ++it) { 425 if (!active) 426 (*it)->scrollAnimator()->contentAreaDidHide(); 427 else 428 (*it)->scrollAnimator()->contentAreaDidShow(); 429 } 430 } 431 } 432 433 focusedOrMainFrame()->selection()->pageActivationChanged(); 434 435 if (m_focusedFrame && isFocused()) 436 dispatchEventsOnWindowAndFocusedNode(m_focusedFrame->document(), active); 437 } 438 439 static void updateFocusCandidateIfNeeded(FocusDirection direction, const FocusCandidate& current, FocusCandidate& candidate, FocusCandidate& closest) 440 { 441 ASSERT(candidate.visibleNode->isElementNode()); 442 ASSERT(candidate.visibleNode->renderer()); 443 444 // Ignore iframes that don't have a src attribute 445 if (frameOwnerElement(candidate) && (!frameOwnerElement(candidate)->contentFrame() || candidate.rect.isEmpty())) 446 return; 447 448 // Ignore off screen child nodes of containers that do not scroll (overflow:hidden) 449 if (candidate.isOffscreen && !canBeScrolledIntoView(direction, candidate)) 450 return; 451 452 distanceDataForNode(direction, current, candidate); 453 if (candidate.distance == maxDistance()) 454 return; 455 456 if (candidate.isOffscreenAfterScrolling && candidate.alignment < Full) 457 return; 458 459 if (closest.isNull()) { 460 closest = candidate; 461 return; 462 } 463 464 IntRect intersectionRect = intersection(candidate.rect, closest.rect); 465 if (!intersectionRect.isEmpty() && !areElementsOnSameLine(closest, candidate)) { 466 // If 2 nodes are intersecting, do hit test to find which node in on top. 467 int x = intersectionRect.x() + intersectionRect.width() / 2; 468 int y = intersectionRect.y() + intersectionRect.height() / 2; 469 HitTestResult result = candidate.visibleNode->document()->page()->mainFrame()->eventHandler()->hitTestResultAtPoint(IntPoint(x, y), false, true); 470 if (candidate.visibleNode->contains(result.innerNode())) { 471 closest = candidate; 472 return; 473 } 474 if (closest.visibleNode->contains(result.innerNode())) 475 return; 476 } 477 478 if (candidate.alignment == closest.alignment) { 479 if (candidate.distance < closest.distance) 480 closest = candidate; 481 return; 482 } 483 484 if (candidate.alignment > closest.alignment) 485 closest = candidate; 486 } 487 488 void FocusController::findFocusCandidateInContainer(Node* container, const IntRect& startingRect, FocusDirection direction, KeyboardEvent* event, FocusCandidate& closest) 489 { 490 ASSERT(container); 491 Node* focusedNode = (focusedFrame() && focusedFrame()->document()) ? focusedFrame()->document()->focusedNode() : 0; 492 493 Node* node = container->firstChild(); 494 FocusCandidate current; 495 current.rect = startingRect; 496 current.focusableNode = focusedNode; 497 current.visibleNode = focusedNode; 498 499 for (; node; node = (node->isFrameOwnerElement() || canScrollInDirection(node, direction)) ? node->traverseNextSibling(container) : node->traverseNextNode(container)) { 500 if (node == focusedNode) 501 continue; 502 503 if (!node->isElementNode()) 504 continue; 505 506 if (!node->isKeyboardFocusable(event) && !node->isFrameOwnerElement() && !canScrollInDirection(node, direction)) 507 continue; 508 509 FocusCandidate candidate = FocusCandidate(node, direction); 510 if (candidate.isNull()) 511 continue; 512 513 candidate.enclosingScrollableBox = container; 514 updateFocusCandidateIfNeeded(direction, current, candidate, closest); 515 } 516 } 517 518 bool FocusController::advanceFocusDirectionallyInContainer(Node* container, const IntRect& startingRect, FocusDirection direction, KeyboardEvent* event) 519 { 520 if (!container || !container->document()) 521 return false; 522 523 IntRect newStartingRect = startingRect; 524 525 if (startingRect.isEmpty()) 526 newStartingRect = virtualRectForDirection(direction, nodeRectInAbsoluteCoordinates(container)); 527 528 // Find the closest node within current container in the direction of the navigation. 529 FocusCandidate focusCandidate; 530 findFocusCandidateInContainer(container, newStartingRect, direction, event, focusCandidate); 531 532 if (focusCandidate.isNull()) { 533 // Nothing to focus, scroll if possible. 534 // NOTE: If no scrolling is performed (i.e. scrollInDirection returns false), the 535 // spatial navigation algorithm will skip this container. 536 return scrollInDirection(container, direction); 537 } 538 539 if (HTMLFrameOwnerElement* frameElement = frameOwnerElement(focusCandidate)) { 540 // If we have an iframe without the src attribute, it will not have a contentFrame(). 541 // We ASSERT here to make sure that 542 // updateFocusCandidateIfNeeded() will never consider such an iframe as a candidate. 543 ASSERT(frameElement->contentFrame()); 544 545 if (focusCandidate.isOffscreenAfterScrolling) { 546 scrollInDirection(focusCandidate.visibleNode->document(), direction); 547 return true; 548 } 549 // Navigate into a new frame. 550 IntRect rect; 551 Node* focusedNode = focusedOrMainFrame()->document()->focusedNode(); 552 if (focusedNode && !hasOffscreenRect(focusedNode)) 553 rect = nodeRectInAbsoluteCoordinates(focusedNode, true /* ignore border */); 554 frameElement->contentFrame()->document()->updateLayoutIgnorePendingStylesheets(); 555 if (!advanceFocusDirectionallyInContainer(frameElement->contentFrame()->document(), rect, direction, event)) { 556 // The new frame had nothing interesting, need to find another candidate. 557 return advanceFocusDirectionallyInContainer(container, nodeRectInAbsoluteCoordinates(focusCandidate.visibleNode, true), direction, event); 558 } 559 return true; 560 } 561 562 if (canScrollInDirection(focusCandidate.visibleNode, direction)) { 563 if (focusCandidate.isOffscreenAfterScrolling) { 564 scrollInDirection(focusCandidate.visibleNode, direction); 565 return true; 566 } 567 // Navigate into a new scrollable container. 568 IntRect startingRect; 569 Node* focusedNode = focusedOrMainFrame()->document()->focusedNode(); 570 if (focusedNode && !hasOffscreenRect(focusedNode)) 571 startingRect = nodeRectInAbsoluteCoordinates(focusedNode, true); 572 return advanceFocusDirectionallyInContainer(focusCandidate.visibleNode, startingRect, direction, event); 573 } 574 if (focusCandidate.isOffscreenAfterScrolling) { 575 Node* container = focusCandidate.enclosingScrollableBox; 576 scrollInDirection(container, direction); 577 return true; 578 } 579 580 // We found a new focus node, navigate to it. 581 Element* element = toElement(focusCandidate.focusableNode); 582 ASSERT(element); 583 584 element->focus(false); 585 return true; 586 } 587 588 bool FocusController::advanceFocusDirectionally(FocusDirection direction, KeyboardEvent* event) 589 { 590 Frame* curFrame = focusedOrMainFrame(); 591 ASSERT(curFrame); 592 593 Document* focusedDocument = curFrame->document(); 594 if (!focusedDocument) 595 return false; 596 597 Node* focusedNode = focusedDocument->focusedNode(); 598 Node* container = focusedDocument; 599 600 if (container->isDocumentNode()) 601 static_cast<Document*>(container)->updateLayoutIgnorePendingStylesheets(); 602 603 // Figure out the starting rect. 604 IntRect startingRect; 605 if (focusedNode) { 606 if (!hasOffscreenRect(focusedNode)) { 607 container = scrollableEnclosingBoxOrParentFrameForNodeInDirection(direction, focusedNode); 608 startingRect = nodeRectInAbsoluteCoordinates(focusedNode, true /* ignore border */); 609 } else if (focusedNode->hasTagName(areaTag)) { 610 HTMLAreaElement* area = static_cast<HTMLAreaElement*>(focusedNode); 611 container = scrollableEnclosingBoxOrParentFrameForNodeInDirection(direction, area->imageElement()); 612 startingRect = virtualRectForAreaElementAndDirection(area, direction); 613 } 614 } 615 616 bool consumed = false; 617 do { 618 consumed = advanceFocusDirectionallyInContainer(container, startingRect, direction, event); 619 startingRect = nodeRectInAbsoluteCoordinates(container, true /* ignore border */); 620 container = scrollableEnclosingBoxOrParentFrameForNodeInDirection(direction, container); 621 if (container && container->isDocumentNode()) 622 static_cast<Document*>(container)->updateLayoutIgnorePendingStylesheets(); 623 } while (!consumed && container); 624 625 return consumed; 626 } 627 628 } // namespace WebCore 629