blob: 6f57a17dc323bcf7b357b277f080adef091352f7 [file] [log] [blame]
/*
* This file is part of the popup menu implementation for <select> elements in WebCore.
*
* Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2006 Michael Emmel mike.emmel@gmail.com
* Copyright (C) 2008 Collabora Ltd.
* Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
* Copyright (C) 2010-2011 Igalia S.L.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "config.h"
#include "GtkPopupMenu.h"
#include <wtf/gobject/GOwnPtr.h>
#include "GtkVersioning.h"
#include <gtk/gtk.h>
#include <wtf/text/CString.h>
namespace WebCore {
static const uint32_t gSearchTimeoutMs = 1000;
GtkPopupMenu::GtkPopupMenu()
: m_popup(gtk_menu_new())
, m_previousKeyEventCharacter(0)
, m_currentlySelectedMenuItem(0)
{
m_keyPressHandlerID = g_signal_connect(m_popup.get(), "key-press-event", G_CALLBACK(GtkPopupMenu::keyPressEventCallback), this);
}
GtkPopupMenu::~GtkPopupMenu()
{
g_signal_handler_disconnect(m_popup.get(), m_keyPressHandlerID);
}
void GtkPopupMenu::clear()
{
gtk_container_foreach(GTK_CONTAINER(m_popup.get()), reinterpret_cast<GtkCallback>(menuRemoveItem), this);
}
void GtkPopupMenu::appendSeparator()
{
GtkWidget* menuItem = gtk_separator_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(m_popup.get()), menuItem);
gtk_widget_show(menuItem);
}
void GtkPopupMenu::appendItem(GtkAction* action)
{
GtkWidget* menuItem = gtk_action_create_menu_item(action);
gtk_widget_set_tooltip_text(menuItem, gtk_action_get_tooltip(action));
g_signal_connect(menuItem, "select", G_CALLBACK(GtkPopupMenu::selectItemCallback), this);
gtk_menu_shell_append(GTK_MENU_SHELL(m_popup.get()), menuItem);
if (gtk_action_is_visible(action))
gtk_widget_show(menuItem);
}
void GtkPopupMenu::popUp(const IntSize& menuSize, const IntPoint& menuPosition, int itemCount, int selectedItem, const GdkEvent* event)
{
resetTypeAheadFindState();
m_menuPosition = menuPosition;
gtk_menu_set_active(GTK_MENU(m_popup.get()), selectedItem);
// This approach follows the one in gtkcombobox.c.
GtkRequisition requisition;
gtk_widget_set_size_request(m_popup.get(), -1, -1);
#ifdef GTK_API_VERSION_2
gtk_widget_size_request(m_popup.get(), &requisition);
#else
gtk_widget_get_preferred_size(m_popup.get(), &requisition, 0);
#endif
gtk_widget_set_size_request(m_popup.get(), std::max(menuSize.width(), requisition.width), -1);
GList* children = gtk_container_get_children(GTK_CONTAINER(m_popup.get()));
GList* p = children;
if (itemCount) {
for (int i = 0; i < itemCount; i++) {
if (i > selectedItem)
break;
GtkWidget* item = reinterpret_cast<GtkWidget*>(p->data);
GtkRequisition itemRequisition;
#ifdef GTK_API_VERSION_2
gtk_widget_get_child_requisition(item, &itemRequisition);
#else
gtk_widget_get_preferred_size(item, &itemRequisition, 0);
#endif
m_menuPosition.setY(m_menuPosition.y() - itemRequisition.height);
p = g_list_next(p);
}
} else {
// Center vertically the empty popup in the combo box area.
m_menuPosition.setY(m_menuPosition.y() - menuSize.height() / 2);
}
g_list_free(children);
guint button;
guint32 activateTime;
if (event) {
button = event->type == GDK_BUTTON_PRESS ? event->button.button : 1;
activateTime = gdk_event_get_time(event);
} else {
button = 1;
activateTime = GDK_CURRENT_TIME;
}
#ifdef GTK_API_VERSION_2
gtk_menu_popup(GTK_MENU(m_popup.get()), 0, 0, reinterpret_cast<GtkMenuPositionFunc>(menuPositionFunction), this, button, activateTime);
#else
gtk_menu_popup_for_device(GTK_MENU(m_popup.get()), event ? gdk_event_get_device(event) : 0, 0, 0,
reinterpret_cast<GtkMenuPositionFunc>(menuPositionFunction), this, 0, button, activateTime);
#endif
}
void GtkPopupMenu::popDown()
{
gtk_menu_popdown(GTK_MENU(m_popup.get()));
resetTypeAheadFindState();
}
void GtkPopupMenu::menuRemoveItem(GtkWidget* widget, GtkPopupMenu* popupMenu)
{
ASSERT(popupMenu->m_popup);
gtk_container_remove(GTK_CONTAINER(popupMenu->m_popup.get()), widget);
}
void GtkPopupMenu::menuPositionFunction(GtkMenu*, gint* x, gint* y, gboolean* pushIn, GtkPopupMenu* popupMenu)
{
*x = popupMenu->m_menuPosition.x();
*y = popupMenu->m_menuPosition.y();
*pushIn = true;
}
void GtkPopupMenu::resetTypeAheadFindState()
{
m_currentlySelectedMenuItem = 0;
m_previousKeyEventCharacter = 0;
m_currentSearchString = "";
}
bool GtkPopupMenu::typeAheadFind(GdkEventKey* event)
{
// If we were given a non-printable character just skip it.
gunichar unicodeCharacter = gdk_keyval_to_unicode(event->keyval);
if (!g_unichar_isprint(unicodeCharacter)) {
resetTypeAheadFindState();
return false;
}
glong charactersWritten;
GOwnPtr<gunichar2> utf16String(g_ucs4_to_utf16(&unicodeCharacter, 1, 0, &charactersWritten, 0));
if (!utf16String) {
resetTypeAheadFindState();
return false;
}
// If the character is the same as the last character, the user is probably trying to
// cycle through the menulist entries. This matches the WebCore behavior for collapsed
// menulists.
bool repeatingCharacter = unicodeCharacter != m_previousKeyEventCharacter;
if (event->time - m_previousKeyEventTimestamp > gSearchTimeoutMs)
m_currentSearchString = String(reinterpret_cast<UChar*>(utf16String.get()), charactersWritten);
else if (repeatingCharacter)
m_currentSearchString.append(String(reinterpret_cast<UChar*>(utf16String.get()), charactersWritten));
m_previousKeyEventTimestamp = event->time;
m_previousKeyEventCharacter = unicodeCharacter;
// Like the Chromium port, we case fold before searching, because
// strncmp does not handle non-ASCII characters.
GOwnPtr<gchar> searchStringWithCaseFolded(g_utf8_casefold(m_currentSearchString.utf8().data(), -1));
size_t prefixLength = strlen(searchStringWithCaseFolded.get());
GList* children = gtk_container_get_children(GTK_CONTAINER(m_popup.get()));
if (!children)
return true;
// If a menu item has already been selected, start searching from the current
// item down the list. This will make multiple key presses of the same character
// advance the selection.
GList* currentChild = children;
if (m_currentlySelectedMenuItem) {
currentChild = g_list_find(children, m_currentlySelectedMenuItem);
if (!currentChild) {
m_currentlySelectedMenuItem = 0;
currentChild = children;
}
// Repeating characters should iterate.
if (repeatingCharacter) {
if (GList* nextChild = g_list_next(currentChild))
currentChild = nextChild;
}
}
GList* firstChild = currentChild;
do {
currentChild = g_list_next(currentChild);
if (!currentChild)
currentChild = children;
GOwnPtr<gchar> itemText(g_utf8_casefold(gtk_menu_item_get_label(GTK_MENU_ITEM(currentChild->data)), -1));
if (!strncmp(searchStringWithCaseFolded.get(), itemText.get(), prefixLength)) {
gtk_menu_shell_select_item(GTK_MENU_SHELL(m_popup.get()), GTK_WIDGET(currentChild->data));
break;
}
} while (currentChild != firstChild);
g_list_free(children);
return true;
}
void GtkPopupMenu::selectItemCallback(GtkMenuItem* item, GtkPopupMenu* popupMenu)
{
popupMenu->m_currentlySelectedMenuItem = GTK_WIDGET(item);
}
gboolean GtkPopupMenu::keyPressEventCallback(GtkWidget* widget, GdkEventKey* event, GtkPopupMenu* popupMenu)
{
return popupMenu->typeAheadFind(event);
}
} // namespace WebCore