1/*
2 * This file is part of the popup menu implementation for <select> elements in WebCore.
3 *
4 * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
5 * Copyright (C) 2006 Michael Emmel mike.emmel@gmail.com
6 * Copyright (C) 2008 Collabora Ltd.
7 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
8 * Copyright (C) 2010 Igalia S.L.
9 *
10 * This library is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU Library General Public
12 * License as published by the Free Software Foundation; either
13 * version 2 of the License, or (at your option) any later version.
14 *
15 * This library is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 * Library General Public License for more details.
19 *
20 * You should have received a copy of the GNU Library General Public License
21 * along with this library; see the file COPYING.LIB.  If not, write to
22 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 * Boston, MA 02110-1301, USA.
24 *
25 */
26
27#include "config.h"
28#include "PopupMenuGtk.h"
29
30#include "FrameView.h"
31#include "GOwnPtr.h"
32#include "GtkVersioning.h"
33#include "HostWindow.h"
34#include "PlatformString.h"
35#include <gdk/gdk.h>
36#include <gtk/gtk.h>
37#include <wtf/text/CString.h>
38
39namespace WebCore {
40
41static const uint32_t gSearchTimeoutMs = 1000;
42
43PopupMenuGtk::PopupMenuGtk(PopupMenuClient* client)
44    : m_popupClient(client)
45    , m_previousKeyEventCharacter(0)
46    , m_currentlySelectedMenuItem(0)
47{
48}
49
50PopupMenuGtk::~PopupMenuGtk()
51{
52    if (m_popup) {
53        g_signal_handlers_disconnect_matched(m_popup.get(), G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, this);
54        hide();
55    }
56}
57
58void PopupMenuGtk::show(const IntRect& rect, FrameView* view, int index)
59{
60    ASSERT(client());
61
62    if (!m_popup) {
63        m_popup = GTK_MENU(gtk_menu_new());
64        g_signal_connect(m_popup.get(), "unmap", G_CALLBACK(PopupMenuGtk::menuUnmapped), this);
65        g_signal_connect(m_popup.get(), "key-press-event", G_CALLBACK(PopupMenuGtk::keyPressEventCallback), this);
66    } else
67        gtk_container_foreach(GTK_CONTAINER(m_popup.get()), reinterpret_cast<GtkCallback>(menuRemoveItem), this);
68
69    int x = 0;
70    int y = 0;
71    GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(view->hostWindow()->platformPageClient()));
72    if (window)
73        gdk_window_get_origin(window, &x, &y);
74    m_menuPosition = view->contentsToWindow(rect.location());
75    m_menuPosition = IntPoint(m_menuPosition.x() + x, m_menuPosition.y() + y + rect.height());
76    m_indexMap.clear();
77
78    const int size = client()->listSize();
79    for (int i = 0; i < size; ++i) {
80        GtkWidget* item;
81        if (client()->itemIsSeparator(i))
82            item = gtk_separator_menu_item_new();
83        else
84            item = gtk_menu_item_new_with_label(client()->itemText(i).utf8().data());
85
86        m_indexMap.add(item, i);
87        g_signal_connect(item, "activate", G_CALLBACK(PopupMenuGtk::menuItemActivated), this);
88        g_signal_connect(item, "select", G_CALLBACK(PopupMenuGtk::selectItemCallback), this);
89
90        // FIXME: Apply the PopupMenuStyle from client()->itemStyle(i)
91        gtk_widget_set_sensitive(item, client()->itemIsEnabled(i));
92        gtk_menu_shell_append(GTK_MENU_SHELL(m_popup.get()), item);
93        gtk_widget_show(item);
94    }
95
96    gtk_menu_set_active(m_popup.get(), index);
97
98
99    // The size calls are directly copied from gtkcombobox.c which is LGPL
100    GtkRequisition requisition;
101    gtk_widget_set_size_request(GTK_WIDGET(m_popup.get()), -1, -1);
102#ifdef GTK_API_VERSION_2
103    gtk_widget_size_request(GTK_WIDGET(m_popup.get()), &requisition);
104#else
105    gtk_widget_get_preferred_size(GTK_WIDGET(m_popup.get()), &requisition, 0);
106#endif
107
108    gtk_widget_set_size_request(GTK_WIDGET(m_popup.get()), std::max(rect.width(), requisition.width), -1);
109
110    GList* children = gtk_container_get_children(GTK_CONTAINER(m_popup.get()));
111    GList* p = children;
112    if (size) {
113        for (int i = 0; i < size; i++) {
114            if (i > index)
115              break;
116
117            GtkWidget* item = reinterpret_cast<GtkWidget*>(p->data);
118            GtkRequisition itemRequisition;
119#ifdef GTK_API_VERSION_2
120            gtk_widget_get_child_requisition(item, &itemRequisition);
121#else
122            gtk_widget_get_preferred_size(item, &itemRequisition, 0);
123#endif
124            m_menuPosition.setY(m_menuPosition.y() - itemRequisition.height);
125
126            p = g_list_next(p);
127        }
128    } else {
129        // Center vertically the empty popup in the combo box area
130        m_menuPosition.setY(m_menuPosition.y() - rect.height() / 2);
131    }
132
133    g_list_free(children);
134    gtk_menu_popup(m_popup.get(), 0, 0, reinterpret_cast<GtkMenuPositionFunc>(menuPositionFunction), this, 0, gtk_get_current_event_time());
135}
136
137void PopupMenuGtk::hide()
138{
139    ASSERT(m_popup);
140    gtk_menu_popdown(m_popup.get());
141}
142
143void PopupMenuGtk::updateFromElement()
144{
145    client()->setTextFromItem(client()->selectedIndex());
146}
147
148void PopupMenuGtk::disconnectClient()
149{
150    m_popupClient = 0;
151}
152
153bool PopupMenuGtk::typeAheadFind(GdkEventKey* event)
154{
155    // If we were given a non-printable character just skip it.
156    gunichar unicodeCharacter = gdk_keyval_to_unicode(event->keyval);
157    if (!unicodeCharacter) {
158        resetTypeAheadFindState();
159        return false;
160    }
161
162    glong charactersWritten;
163    GOwnPtr<gunichar2> utf16String(g_ucs4_to_utf16(&unicodeCharacter, 1, 0, &charactersWritten, 0));
164    if (!utf16String) {
165        resetTypeAheadFindState();
166        return false;
167    }
168
169    // If the character is the same as the last character, the user is probably trying to
170    // cycle through the menulist entries. This matches the WebCore behavior for collapsed
171    // menulists.
172    bool repeatingCharacter = unicodeCharacter != m_previousKeyEventCharacter;
173    if (event->time - m_previousKeyEventTimestamp > gSearchTimeoutMs)
174        m_currentSearchString = String(static_cast<UChar*>(utf16String.get()), charactersWritten);
175    else if (repeatingCharacter)
176        m_currentSearchString.append(String(static_cast<UChar*>(utf16String.get()), charactersWritten));
177
178    m_previousKeyEventTimestamp = event->time;
179    m_previousKeyEventCharacter = unicodeCharacter;
180
181    // Like the Chromium port, we case fold before searching, because
182    // strncmp does not handle non-ASCII characters.
183    GOwnPtr<gchar> searchStringWithCaseFolded(g_utf8_casefold(m_currentSearchString.utf8().data(), -1));
184    size_t prefixLength = strlen(searchStringWithCaseFolded.get());
185
186    GList* children = gtk_container_get_children(GTK_CONTAINER(m_popup.get()));
187    if (!children)
188        return true;
189
190    // If a menu item has already been selected, start searching from the current
191    // item down the list. This will make multiple key presses of the same character
192    // advance the selection.
193    GList* currentChild = children;
194    if (m_currentlySelectedMenuItem) {
195        currentChild = g_list_find(children, m_currentlySelectedMenuItem);
196        if (!currentChild) {
197            m_currentlySelectedMenuItem = 0;
198            currentChild = children;
199        }
200
201        // Repeating characters should iterate.
202        if (repeatingCharacter) {
203            if (GList* nextChild = g_list_next(currentChild))
204                currentChild = nextChild;
205        }
206    }
207
208    GList* firstChild = currentChild;
209    do {
210        currentChild = g_list_next(currentChild);
211        if (!currentChild)
212            currentChild = children;
213
214        GOwnPtr<gchar> itemText(g_utf8_casefold(gtk_menu_item_get_label(GTK_MENU_ITEM(currentChild->data)), -1));
215        if (!strncmp(searchStringWithCaseFolded.get(), itemText.get(), prefixLength)) {
216            gtk_menu_shell_select_item(GTK_MENU_SHELL(m_popup.get()), GTK_WIDGET(currentChild->data));
217            return true;
218        }
219    } while (currentChild != firstChild);
220
221    return true;
222}
223
224void PopupMenuGtk::menuItemActivated(GtkMenuItem* item, PopupMenuGtk* that)
225{
226    ASSERT(that->client());
227    ASSERT(that->m_indexMap.contains(GTK_WIDGET(item)));
228    that->client()->valueChanged(that->m_indexMap.get(GTK_WIDGET(item)));
229}
230
231void PopupMenuGtk::menuUnmapped(GtkWidget*, PopupMenuGtk* that)
232{
233    ASSERT(that->client());
234    that->resetTypeAheadFindState();
235    that->client()->popupDidHide();
236}
237
238void PopupMenuGtk::menuPositionFunction(GtkMenu*, gint* x, gint* y, gboolean* pushIn, PopupMenuGtk* that)
239{
240    *x = that->m_menuPosition.x();
241    *y = that->m_menuPosition.y();
242    *pushIn = true;
243}
244
245void PopupMenuGtk::resetTypeAheadFindState()
246{
247    m_currentlySelectedMenuItem = 0;
248    m_previousKeyEventCharacter = 0;
249    m_currentSearchString = "";
250}
251
252void PopupMenuGtk::menuRemoveItem(GtkWidget* widget, PopupMenuGtk* that)
253{
254    ASSERT(that->m_popup);
255    gtk_container_remove(GTK_CONTAINER(that->m_popup.get()), widget);
256}
257
258int PopupMenuGtk::selectItemCallback(GtkMenuItem* item, PopupMenuGtk* that)
259{
260    that->m_currentlySelectedMenuItem = GTK_WIDGET(item);
261    return FALSE;
262}
263
264int PopupMenuGtk::keyPressEventCallback(GtkWidget* widget, GdkEventKey* event, PopupMenuGtk* that)
265{
266    return that->typeAheadFind(event);
267}
268
269}
270
271