1 /* 2 * Copyright (C) 2009 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 #include "config.h" 32 #include "ContextMenuClientImpl.h" 33 34 #include "CSSPropertyNames.h" 35 #include "CSSStyleDeclaration.h" 36 #include "ContextMenu.h" 37 #include "ContextMenuController.h" 38 #include "Document.h" 39 #include "DocumentLoader.h" 40 #include "Editor.h" 41 #include "EventHandler.h" 42 #include "FrameLoader.h" 43 #include "FrameView.h" 44 #include "HistoryItem.h" 45 #include "HitTestResult.h" 46 #include "HTMLMediaElement.h" 47 #include "HTMLNames.h" 48 #include "HTMLPlugInImageElement.h" 49 #include "KURL.h" 50 #include "MediaError.h" 51 #include "Page.h" 52 #include "PlatformString.h" 53 #include "RenderWidget.h" 54 #include "TextBreakIterator.h" 55 #include "Widget.h" 56 57 #include "WebContextMenuData.h" 58 #include "WebDataSourceImpl.h" 59 #include "WebFrameImpl.h" 60 #include "WebMenuItemInfo.h" 61 #include "WebPlugin.h" 62 #include "WebPluginContainerImpl.h" 63 #include "WebPoint.h" 64 #include "WebSpellCheckClient.h" 65 #include "WebString.h" 66 #include "WebURL.h" 67 #include "WebURLResponse.h" 68 #include "WebVector.h" 69 #include "WebViewClient.h" 70 #include "WebViewImpl.h" 71 72 using namespace WebCore; 73 74 namespace WebKit { 75 76 // Figure out the URL of a page or subframe. Returns |page_type| as the type, 77 // which indicates page or subframe, or ContextNodeType::NONE if the URL could not 78 // be determined for some reason. 79 static WebURL urlFromFrame(Frame* frame) 80 { 81 if (frame) { 82 DocumentLoader* dl = frame->loader()->documentLoader(); 83 if (dl) { 84 WebDataSource* ds = WebDataSourceImpl::fromDocumentLoader(dl); 85 if (ds) 86 return ds->hasUnreachableURL() ? ds->unreachableURL() : ds->request().url(); 87 } 88 } 89 return WebURL(); 90 } 91 92 // Helper function to determine whether text is a single word. 93 static bool isASingleWord(const String& text) 94 { 95 TextBreakIterator* it = wordBreakIterator(text.characters(), text.length()); 96 return it && textBreakNext(it) == static_cast<int>(text.length()); 97 } 98 99 // Helper function to get misspelled word on which context menu 100 // is to be evolked. This function also sets the word on which context menu 101 // has been evoked to be the selected word, as required. This function changes 102 // the selection only when there were no selected characters on OS X. 103 static String selectMisspelledWord(const ContextMenu* defaultMenu, Frame* selectedFrame) 104 { 105 // First select from selectedText to check for multiple word selection. 106 String misspelledWord = selectedFrame->editor()->selectedText().stripWhiteSpace(); 107 108 // If some texts were already selected, we don't change the selection. 109 if (!misspelledWord.isEmpty()) { 110 // Don't provide suggestions for multiple words. 111 if (!isASingleWord(misspelledWord)) 112 return String(); 113 return misspelledWord; 114 } 115 116 // Selection is empty, so change the selection to the word under the cursor. 117 HitTestResult hitTestResult = selectedFrame->eventHandler()-> 118 hitTestResultAtPoint(selectedFrame->page()->contextMenuController()->hitTestResult().point(), true); 119 Node* innerNode = hitTestResult.innerNode(); 120 VisiblePosition pos(innerNode->renderer()->positionForPoint( 121 hitTestResult.localPoint())); 122 123 if (pos.isNull()) 124 return misspelledWord; // It is empty. 125 126 WebFrameImpl::selectWordAroundPosition(selectedFrame, pos); 127 misspelledWord = selectedFrame->editor()->selectedText().stripWhiteSpace(); 128 129 #if OS(DARWIN) 130 // If misspelled word is still empty, then that portion should not be 131 // selected. Set the selection to that position only, and do not expand. 132 if (misspelledWord.isEmpty()) 133 selectedFrame->selection()->setSelection(VisibleSelection(pos)); 134 #else 135 // On non-Mac, right-click should not make a range selection in any case. 136 selectedFrame->selection()->setSelection(VisibleSelection(pos)); 137 #endif 138 return misspelledWord; 139 } 140 141 PlatformMenuDescription ContextMenuClientImpl::getCustomMenuFromDefaultItems( 142 ContextMenu* defaultMenu) 143 { 144 // Displaying the context menu in this function is a big hack as we don't 145 // have context, i.e. whether this is being invoked via a script or in 146 // response to user input (Mouse event WM_RBUTTONDOWN, 147 // Keyboard events KeyVK_APPS, Shift+F10). Check if this is being invoked 148 // in response to the above input events before popping up the context menu. 149 if (!m_webView->contextMenuAllowed()) 150 return 0; 151 152 HitTestResult r = m_webView->page()->contextMenuController()->hitTestResult(); 153 Frame* selectedFrame = r.innerNonSharedNode()->document()->frame(); 154 155 WebContextMenuData data; 156 data.mousePosition = selectedFrame->view()->contentsToWindow(r.point()); 157 158 // Compute edit flags. 159 data.editFlags = WebContextMenuData::CanDoNone; 160 if (m_webView->focusedWebCoreFrame()->editor()->canUndo()) 161 data.editFlags |= WebContextMenuData::CanUndo; 162 if (m_webView->focusedWebCoreFrame()->editor()->canRedo()) 163 data.editFlags |= WebContextMenuData::CanRedo; 164 if (m_webView->focusedWebCoreFrame()->editor()->canCut()) 165 data.editFlags |= WebContextMenuData::CanCut; 166 if (m_webView->focusedWebCoreFrame()->editor()->canCopy()) 167 data.editFlags |= WebContextMenuData::CanCopy; 168 if (m_webView->focusedWebCoreFrame()->editor()->canPaste()) 169 data.editFlags |= WebContextMenuData::CanPaste; 170 if (m_webView->focusedWebCoreFrame()->editor()->canDelete()) 171 data.editFlags |= WebContextMenuData::CanDelete; 172 // We can always select all... 173 data.editFlags |= WebContextMenuData::CanSelectAll; 174 data.editFlags |= WebContextMenuData::CanTranslate; 175 176 // Links, Images, Media tags, and Image/Media-Links take preference over 177 // all else. 178 data.linkURL = r.absoluteLinkURL(); 179 180 if (!r.absoluteImageURL().isEmpty()) { 181 data.srcURL = r.absoluteImageURL(); 182 data.mediaType = WebContextMenuData::MediaTypeImage; 183 } else if (!r.absoluteMediaURL().isEmpty()) { 184 data.srcURL = r.absoluteMediaURL(); 185 186 // We know that if absoluteMediaURL() is not empty, then this 187 // is a media element. 188 HTMLMediaElement* mediaElement = 189 static_cast<HTMLMediaElement*>(r.innerNonSharedNode()); 190 if (mediaElement->hasTagName(HTMLNames::videoTag)) 191 data.mediaType = WebContextMenuData::MediaTypeVideo; 192 else if (mediaElement->hasTagName(HTMLNames::audioTag)) 193 data.mediaType = WebContextMenuData::MediaTypeAudio; 194 195 if (mediaElement->error()) 196 data.mediaFlags |= WebContextMenuData::MediaInError; 197 if (mediaElement->paused()) 198 data.mediaFlags |= WebContextMenuData::MediaPaused; 199 if (mediaElement->muted()) 200 data.mediaFlags |= WebContextMenuData::MediaMuted; 201 if (mediaElement->loop()) 202 data.mediaFlags |= WebContextMenuData::MediaLoop; 203 if (mediaElement->supportsSave()) 204 data.mediaFlags |= WebContextMenuData::MediaCanSave; 205 if (mediaElement->hasAudio()) 206 data.mediaFlags |= WebContextMenuData::MediaHasAudio; 207 if (mediaElement->hasVideo()) 208 data.mediaFlags |= WebContextMenuData::MediaHasVideo; 209 if (mediaElement->controls()) 210 data.mediaFlags |= WebContextMenuData::MediaControlRootElement; 211 } else if (r.innerNonSharedNode()->hasTagName(HTMLNames::objectTag) 212 || r.innerNonSharedNode()->hasTagName(HTMLNames::embedTag)) { 213 RenderObject* object = r.innerNonSharedNode()->renderer(); 214 if (object && object->isWidget()) { 215 Widget* widget = toRenderWidget(object)->widget(); 216 if (widget && widget->isPluginContainer()) { 217 data.mediaType = WebContextMenuData::MediaTypePlugin; 218 WebPluginContainerImpl* plugin = static_cast<WebPluginContainerImpl*>(widget); 219 WebString text = plugin->plugin()->selectionAsText(); 220 if (!text.isEmpty()) { 221 data.selectedText = text; 222 data.editFlags |= WebContextMenuData::CanCopy; 223 } 224 data.editFlags &= ~WebContextMenuData::CanTranslate; 225 data.linkURL = plugin->plugin()->linkAtPosition(data.mousePosition); 226 if (plugin->plugin()->supportsPaginatedPrint()) 227 data.mediaFlags |= WebContextMenuData::MediaCanPrint; 228 229 HTMLPlugInImageElement* pluginElement = static_cast<HTMLPlugInImageElement*>(r.innerNonSharedNode()); 230 data.srcURL = pluginElement->document()->completeURL(pluginElement->url()); 231 data.mediaFlags |= WebContextMenuData::MediaCanSave; 232 } 233 } 234 } 235 236 data.isImageBlocked = 237 (data.mediaType == WebContextMenuData::MediaTypeImage) && !r.image(); 238 239 // If it's not a link, an image, a media element, or an image/media link, 240 // show a selection menu or a more generic page menu. 241 data.frameEncoding = selectedFrame->document()->loader()->writer()->encoding(); 242 243 // Send the frame and page URLs in any case. 244 data.pageURL = urlFromFrame(m_webView->mainFrameImpl()->frame()); 245 if (selectedFrame != m_webView->mainFrameImpl()->frame()) { 246 data.frameURL = urlFromFrame(selectedFrame); 247 RefPtr<HistoryItem> historyItem = selectedFrame->loader()->history()->currentItem(); 248 if (historyItem) 249 data.frameHistoryItem = WebHistoryItem(historyItem); 250 } 251 252 if (r.isSelected()) 253 data.selectedText = selectedFrame->editor()->selectedText().stripWhiteSpace(); 254 255 if (r.isContentEditable()) { 256 data.isEditable = true; 257 if (m_webView->focusedWebCoreFrame()->editor()->isContinuousSpellCheckingEnabled()) { 258 data.isSpellCheckingEnabled = true; 259 // Spellchecking might be enabled for the field, but could be disabled on the node. 260 if (m_webView->focusedWebCoreFrame()->editor()->isSpellCheckingEnabledInFocusedNode()) { 261 data.misspelledWord = selectMisspelledWord(defaultMenu, selectedFrame); 262 if (m_webView->spellCheckClient()) { 263 int misspelledOffset, misspelledLength; 264 m_webView->spellCheckClient()->spellCheck( 265 data.misspelledWord, misspelledOffset, misspelledLength, 266 &data.dictionarySuggestions); 267 if (!misspelledLength) 268 data.misspelledWord.reset(); 269 } 270 } 271 } 272 } 273 274 #if OS(DARWIN) 275 if (selectedFrame->editor()->selectionHasStyle(CSSPropertyDirection, "ltr") != FalseTriState) 276 data.writingDirectionLeftToRight |= WebContextMenuData::CheckableMenuItemChecked; 277 if (selectedFrame->editor()->selectionHasStyle(CSSPropertyDirection, "rtl") != FalseTriState) 278 data.writingDirectionRightToLeft |= WebContextMenuData::CheckableMenuItemChecked; 279 #endif // OS(DARWIN) 280 281 // Now retrieve the security info. 282 DocumentLoader* dl = selectedFrame->loader()->documentLoader(); 283 WebDataSource* ds = WebDataSourceImpl::fromDocumentLoader(dl); 284 if (ds) 285 data.securityInfo = ds->response().securityInfo(); 286 287 // Filter out custom menu elements and add them into the data. 288 populateCustomMenuItems(defaultMenu, &data); 289 290 data.node = r.innerNonSharedNode(); 291 292 WebFrame* selected_web_frame = WebFrameImpl::fromFrame(selectedFrame); 293 if (m_webView->client()) 294 m_webView->client()->showContextMenu(selected_web_frame, data); 295 296 return 0; 297 } 298 299 void ContextMenuClientImpl::populateCustomMenuItems(WebCore::ContextMenu* defaultMenu, WebContextMenuData* data) 300 { 301 Vector<WebMenuItemInfo> customItems; 302 for (size_t i = 0; i < defaultMenu->itemCount(); ++i) { 303 ContextMenuItem* inputItem = defaultMenu->itemAtIndex(i, defaultMenu->platformDescription()); 304 if (inputItem->action() < ContextMenuItemBaseCustomTag || inputItem->action() > ContextMenuItemLastCustomTag) 305 continue; 306 307 WebMenuItemInfo outputItem; 308 outputItem.label = inputItem->title(); 309 outputItem.enabled = inputItem->enabled(); 310 outputItem.checked = inputItem->checked(); 311 outputItem.action = static_cast<unsigned>(inputItem->action() - ContextMenuItemBaseCustomTag); 312 switch (inputItem->type()) { 313 case ActionType: 314 outputItem.type = WebMenuItemInfo::Option; 315 break; 316 case CheckableActionType: 317 outputItem.type = WebMenuItemInfo::CheckableOption; 318 break; 319 case SeparatorType: 320 outputItem.type = WebMenuItemInfo::Separator; 321 break; 322 case SubmenuType: 323 outputItem.type = WebMenuItemInfo::Group; 324 break; 325 } 326 customItems.append(outputItem); 327 } 328 329 WebVector<WebMenuItemInfo> outputItems(customItems.size()); 330 for (size_t i = 0; i < customItems.size(); ++i) 331 outputItems[i] = customItems[i]; 332 data->customItems.swap(outputItems); 333 } 334 335 } // namespace WebKit 336