Home | History | Annotate | Download | only in gtk
      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