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
72using namespace WebCore;
73
74namespace 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.
79static 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.
93static 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.
103static 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
141PlatformMenuDescription 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
299void 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