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
39 namespace WebCore {
40
41 static const uint32_t gSearchTimeoutMs = 1000;
42
PopupMenuGtk(PopupMenuClient * client)43 PopupMenuGtk::PopupMenuGtk(PopupMenuClient* client)
44 : m_popupClient(client)
45 , m_previousKeyEventCharacter(0)
46 , m_currentlySelectedMenuItem(0)
47 {
48 }
49
~PopupMenuGtk()50 PopupMenuGtk::~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
show(const IntRect & rect,FrameView * view,int index)58 void 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
hide()137 void PopupMenuGtk::hide()
138 {
139 ASSERT(m_popup);
140 gtk_menu_popdown(m_popup.get());
141 }
142
updateFromElement()143 void PopupMenuGtk::updateFromElement()
144 {
145 client()->setTextFromItem(client()->selectedIndex());
146 }
147
disconnectClient()148 void PopupMenuGtk::disconnectClient()
149 {
150 m_popupClient = 0;
151 }
152
typeAheadFind(GdkEventKey * event)153 bool 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
menuItemActivated(GtkMenuItem * item,PopupMenuGtk * that)224 void 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
menuUnmapped(GtkWidget *,PopupMenuGtk * that)231 void PopupMenuGtk::menuUnmapped(GtkWidget*, PopupMenuGtk* that)
232 {
233 ASSERT(that->client());
234 that->resetTypeAheadFindState();
235 that->client()->popupDidHide();
236 }
237
menuPositionFunction(GtkMenu *,gint * x,gint * y,gboolean * pushIn,PopupMenuGtk * that)238 void 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
resetTypeAheadFindState()245 void PopupMenuGtk::resetTypeAheadFindState()
246 {
247 m_currentlySelectedMenuItem = 0;
248 m_previousKeyEventCharacter = 0;
249 m_currentSearchString = "";
250 }
251
menuRemoveItem(GtkWidget * widget,PopupMenuGtk * that)252 void 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
selectItemCallback(GtkMenuItem * item,PopupMenuGtk * that)258 int PopupMenuGtk::selectItemCallback(GtkMenuItem* item, PopupMenuGtk* that)
259 {
260 that->m_currentlySelectedMenuItem = GTK_WIDGET(item);
261 return FALSE;
262 }
263
keyPressEventCallback(GtkWidget * widget,GdkEventKey * event,PopupMenuGtk * that)264 int PopupMenuGtk::keyPressEventCallback(GtkWidget* widget, GdkEventKey* event, PopupMenuGtk* that)
265 {
266 return that->typeAheadFind(event);
267 }
268
269 }
270
271