1/*
2 * Copyright (C) 2011 Igalia S.L.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "WebContextMenuProxyGtk.h"
28
29#if ENABLE(CONTEXT_MENUS)
30
31#include "NativeWebMouseEvent.h"
32#include "WebContextMenuItemData.h"
33#include "WebKitWebViewBasePrivate.h"
34#include "WebPageProxy.h"
35#include <WebCore/GtkUtilities.h>
36#include <gtk/gtk.h>
37#include <wtf/text/CString.h>
38
39
40static const char* gContextMenuActionId = "webkit-context-menu-action";
41
42using namespace WebCore;
43
44namespace WebKit {
45
46static void contextMenuItemActivatedCallback(GtkAction* action, WebPageProxy* page)
47{
48    gboolean isToggle = GTK_IS_TOGGLE_ACTION(action);
49    WebKit::WebContextMenuItemData item(isToggle ? WebCore::CheckableActionType : WebCore::ActionType,
50        static_cast<WebCore::ContextMenuAction>(GPOINTER_TO_INT(g_object_get_data(G_OBJECT(action), gContextMenuActionId))),
51        String::fromUTF8(gtk_action_get_label(action)), gtk_action_get_sensitive(action),
52        isToggle ? gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action)) : false);
53    page->contextMenuItemSelected(item);
54}
55
56static void contextMenuItemVisibilityChanged(GtkAction*, GParamSpec*, WebContextMenuProxyGtk* contextMenuProxy)
57{
58    GtkMenu* menu = contextMenuProxy->gtkMenu();
59    if (!menu)
60        return;
61
62    GUniquePtr<GList> items(gtk_container_get_children(GTK_CONTAINER(menu)));
63    bool previousVisibleItemIsNotASeparator = false;
64    GtkWidget* lastItemVisibleSeparator = 0;
65    for (GList* iter = items.get(); iter; iter = g_list_next(iter)) {
66        GtkWidget* widget = GTK_WIDGET(iter->data);
67
68        if (GTK_IS_SEPARATOR_MENU_ITEM(widget)) {
69            if (previousVisibleItemIsNotASeparator) {
70                gtk_widget_show(widget);
71                lastItemVisibleSeparator = widget;
72                previousVisibleItemIsNotASeparator = false;
73            } else
74                gtk_widget_hide(widget);
75        } else if (gtk_widget_get_visible(widget)) {
76            lastItemVisibleSeparator = 0;
77            previousVisibleItemIsNotASeparator = true;
78        }
79    }
80
81    if (lastItemVisibleSeparator)
82        gtk_widget_hide(lastItemVisibleSeparator);
83}
84
85void WebContextMenuProxyGtk::append(ContextMenuItem& menuItem)
86{
87    unsigned long signalHandlerId;
88    GtkAction* action = menuItem.gtkAction();
89    if (action) {
90        switch (menuItem.type()) {
91        case ActionType:
92        case CheckableActionType:
93            g_object_set_data(G_OBJECT(action), gContextMenuActionId, GINT_TO_POINTER(menuItem.action()));
94            signalHandlerId = g_signal_connect(action, "activate", G_CALLBACK(contextMenuItemActivatedCallback), m_page);
95            m_signalHandlers.set(signalHandlerId, action);
96            // Fall through.
97        case SubmenuType:
98            signalHandlerId = g_signal_connect(action, "notify::visible", G_CALLBACK(contextMenuItemVisibilityChanged), this);
99            m_signalHandlers.set(signalHandlerId, action);
100            break;
101        case SeparatorType:
102            break;
103        }
104    }
105
106    m_menu.appendItem(menuItem);
107}
108
109// Populate the context menu ensuring that:
110//  - There aren't separators next to each other.
111//  - There aren't separators at the beginning of the menu.
112//  - There aren't separators at the end of the menu.
113void WebContextMenuProxyGtk::populate(Vector<ContextMenuItem>& items)
114{
115    bool previousIsSeparator = false;
116    bool isEmpty = true;
117    for (size_t i = 0; i < items.size(); i++) {
118        ContextMenuItem& menuItem = items.at(i);
119        if (menuItem.type() == SeparatorType) {
120            previousIsSeparator = true;
121            continue;
122        }
123
124        if (previousIsSeparator && !isEmpty)
125            append(items.at(i - 1));
126        previousIsSeparator = false;
127
128        append(menuItem);
129        isEmpty = false;
130    }
131}
132
133void WebContextMenuProxyGtk::populate(const Vector<WebContextMenuItemData>& items)
134{
135    for (size_t i = 0; i < items.size(); i++) {
136        ContextMenuItem menuitem = items.at(i).core();
137        append(menuitem);
138    }
139}
140
141void WebContextMenuProxyGtk::showContextMenu(const WebCore::IntPoint& position, const Vector<WebContextMenuItemData>& items, const ContextMenuContextData&)
142{
143    if (!items.isEmpty())
144        populate(items);
145
146    if (!m_menu.itemCount())
147        return;
148
149    m_popupPosition = convertWidgetPointToScreenPoint(m_webView, position);
150
151    // Display menu initiated by right click (mouse button pressed = 3).
152    NativeWebMouseEvent* mouseEvent = m_page->currentlyProcessedMouseDownEvent();
153    const GdkEvent* event = mouseEvent ? mouseEvent->nativeEvent() : 0;
154    gtk_menu_attach_to_widget(m_menu.platformDescription(), GTK_WIDGET(m_webView), 0);
155    gtk_menu_popup(m_menu.platformDescription(), 0, 0, reinterpret_cast<GtkMenuPositionFunc>(menuPositionFunction), this,
156                   event ? event->button.button : 3, event ? event->button.time : GDK_CURRENT_TIME);
157}
158
159void WebContextMenuProxyGtk::hideContextMenu()
160{
161    gtk_menu_popdown(m_menu.platformDescription());
162}
163
164WebContextMenuProxyGtk::WebContextMenuProxyGtk(GtkWidget* webView, WebPageProxy* page)
165    : m_webView(webView)
166    , m_page(page)
167{
168    webkitWebViewBaseSetActiveContextMenuProxy(WEBKIT_WEB_VIEW_BASE(m_webView), this);
169}
170
171WebContextMenuProxyGtk::~WebContextMenuProxyGtk()
172{
173    for (auto iter = m_signalHandlers.begin(); iter != m_signalHandlers.end(); ++iter)
174        g_signal_handler_disconnect(iter->value, iter->key);
175
176    webkitWebViewBaseSetActiveContextMenuProxy(WEBKIT_WEB_VIEW_BASE(m_webView), 0);
177}
178
179void WebContextMenuProxyGtk::menuPositionFunction(GtkMenu* menu, gint* x, gint* y, gboolean* pushIn, WebContextMenuProxyGtk* popupMenu)
180{
181    GtkRequisition menuSize;
182    gtk_widget_get_preferred_size(GTK_WIDGET(menu), &menuSize, 0);
183
184    GdkScreen* screen = gtk_widget_get_screen(popupMenu->m_webView);
185    *x = popupMenu->m_popupPosition.x();
186    if ((*x + menuSize.width) >= gdk_screen_get_width(screen))
187        *x -= menuSize.width;
188
189    *y = popupMenu->m_popupPosition.y();
190    if ((*y + menuSize.height) >= gdk_screen_get_height(screen))
191        *y -= menuSize.height;
192
193    *pushIn = FALSE;
194}
195
196} // namespace WebKit
197#endif // ENABLE(CONTEXT_MENUS)
198