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 (at) 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 43 PopupMenuGtk::PopupMenuGtk(PopupMenuClient* client) 44 : m_popupClient(client) 45 , m_previousKeyEventCharacter(0) 46 , m_currentlySelectedMenuItem(0) 47 { 48 } 49 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 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 137 void PopupMenuGtk::hide() 138 { 139 ASSERT(m_popup); 140 gtk_menu_popdown(m_popup.get()); 141 } 142 143 void PopupMenuGtk::updateFromElement() 144 { 145 client()->setTextFromItem(client()->selectedIndex()); 146 } 147 148 void PopupMenuGtk::disconnectClient() 149 { 150 m_popupClient = 0; 151 } 152 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 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 231 void PopupMenuGtk::menuUnmapped(GtkWidget*, PopupMenuGtk* that) 232 { 233 ASSERT(that->client()); 234 that->resetTypeAheadFindState(); 235 that->client()->popupDidHide(); 236 } 237 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 245 void PopupMenuGtk::resetTypeAheadFindState() 246 { 247 m_currentlySelectedMenuItem = 0; 248 m_previousKeyEventCharacter = 0; 249 m_currentSearchString = ""; 250 } 251 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 258 int PopupMenuGtk::selectItemCallback(GtkMenuItem* item, PopupMenuGtk* that) 259 { 260 that->m_currentlySelectedMenuItem = GTK_WIDGET(item); 261 return FALSE; 262 } 263 264 int PopupMenuGtk::keyPressEventCallback(GtkWidget* widget, GdkEventKey* event, PopupMenuGtk* that) 265 { 266 return that->typeAheadFind(event); 267 } 268 269 } 270 271