summaryrefslogtreecommitdiff
path: root/Source/WebKit2/UIProcess/gtk/WebPopupMenuProxyGtk.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/WebKit2/UIProcess/gtk/WebPopupMenuProxyGtk.cpp')
-rw-r--r--Source/WebKit2/UIProcess/gtk/WebPopupMenuProxyGtk.cpp237
1 files changed, 193 insertions, 44 deletions
diff --git a/Source/WebKit2/UIProcess/gtk/WebPopupMenuProxyGtk.cpp b/Source/WebKit2/UIProcess/gtk/WebPopupMenuProxyGtk.cpp
index 782730de7..2eb0ec488 100644
--- a/Source/WebKit2/UIProcess/gtk/WebPopupMenuProxyGtk.cpp
+++ b/Source/WebKit2/UIProcess/gtk/WebPopupMenuProxyGtk.cpp
@@ -29,27 +29,28 @@
#include "NativeWebMouseEvent.h"
#include "WebPopupItem.h"
#include <WebCore/GtkUtilities.h>
+#include <WebCore/IntRect.h>
#include <gtk/gtk.h>
-#include <wtf/gobject/GUniquePtr.h>
+#include <wtf/glib/GUniquePtr.h>
#include <wtf/text/CString.h>
using namespace WebCore;
namespace WebKit {
-WebPopupMenuProxyGtk::WebPopupMenuProxyGtk(GtkWidget* webView, WebPopupMenuProxy::Client* client)
+WebPopupMenuProxyGtk::WebPopupMenuProxyGtk(GtkWidget* webView, WebPopupMenuProxy::Client& client)
: WebPopupMenuProxy(client)
, m_webView(webView)
- , m_activeItem(-1)
+ , m_popup(gtk_menu_new())
+ , m_dismissMenuTimer(RunLoop::main(), this, &WebPopupMenuProxyGtk::dismissMenuTimerFired)
{
+ g_signal_connect(m_popup, "key-press-event", G_CALLBACK(keyPressEventCallback), this);
+ g_signal_connect(m_popup, "unmap", G_CALLBACK(menuUnmappedCallback), this);
}
WebPopupMenuProxyGtk::~WebPopupMenuProxyGtk()
{
- if (m_popup) {
- g_signal_handlers_disconnect_matched(m_popup->platformMenu(), G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, this);
- hidePopupMenu();
- }
+ cancelTracking();
}
GtkAction* WebPopupMenuProxyGtk::createGtkActionForMenuItem(const WebPopupItem& item, int itemIndex)
@@ -63,74 +64,222 @@ GtkAction* WebPopupMenuProxyGtk::createGtkActionForMenuItem(const WebPopupItem&
return action;
}
-void WebPopupMenuProxyGtk::showPopupMenu(const IntRect& rect, TextDirection textDirection, double pageScaleFactor, const Vector<WebPopupItem>& items, const PlatformPopupMenuData& data, int32_t selectedIndex)
+void WebPopupMenuProxyGtk::populatePopupMenu(const Vector<WebPopupItem>& items)
{
- if (m_popup)
- m_popup->clear();
- else
- m_popup = GtkPopupMenu::create();
-
- const int size = items.size();
- for (int i = 0; i < size; i++) {
- if (items[i].m_type == WebPopupItem::Separator)
- m_popup->appendSeparator();
- else {
- GRefPtr<GtkAction> action = adoptGRef(createGtkActionForMenuItem(items[i], i));
- m_popup->appendItem(action.get());
+ int itemIndex = 0;
+ for (const auto& item : items) {
+ if (item.m_type == WebPopupItem::Separator) {
+ GtkWidget* menuItem = gtk_separator_menu_item_new();
+ gtk_menu_shell_append(GTK_MENU_SHELL(m_popup), menuItem);
+ gtk_widget_show(menuItem);
+ } else {
+ GRefPtr<GtkAction> action = adoptGRef(createGtkActionForMenuItem(item, itemIndex));
+ GtkWidget* menuItem = gtk_action_create_menu_item(action.get());
+ gtk_widget_set_tooltip_text(menuItem, gtk_action_get_tooltip(action.get()));
+ g_signal_connect(menuItem, "select", G_CALLBACK(selectItemCallback), this);
+ gtk_menu_shell_append(GTK_MENU_SHELL(m_popup), menuItem);
+
+ if (gtk_action_is_visible(action.get()))
+ gtk_widget_show(menuItem);
}
+ itemIndex++;
}
+}
+
+void WebPopupMenuProxyGtk::showPopupMenu(const IntRect& rect, TextDirection, double /* pageScaleFactor */, const Vector<WebPopupItem>& items, const PlatformPopupMenuData&, int32_t selectedIndex)
+{
+ m_dismissMenuTimer.stop();
+
+ populatePopupMenu(items);
+ gtk_menu_set_active(GTK_MENU(m_popup), selectedIndex);
+
+ resetTypeAheadFindState();
IntPoint menuPosition = convertWidgetPointToScreenPoint(m_webView, rect.location());
menuPosition.move(0, rect.height());
- gulong unmapHandler = g_signal_connect(m_popup->platformMenu(), "unmap", G_CALLBACK(menuUnmapped), this);
- m_popup->popUp(rect.size(), menuPosition, size, selectedIndex, m_client->currentlyProcessedMouseDownEvent() ? m_client->currentlyProcessedMouseDownEvent()->nativeEvent() : 0);
+ // This approach follows the one in gtkcombobox.c.
+ GtkRequisition requisition;
+ gtk_widget_set_size_request(m_popup, -1, -1);
+ gtk_widget_get_preferred_size(m_popup, &requisition, nullptr);
+ gtk_widget_set_size_request(m_popup, std::max(rect.width(), requisition.width), -1);
+
+ if (int itemCount = items.size()) {
+ GUniquePtr<GList> children(gtk_container_get_children(GTK_CONTAINER(m_popup)));
+ int i;
+ GList* child;
+ for (i = 0, child = children.get(); i < itemCount; i++, child = g_list_next(child)) {
+ if (i > selectedIndex)
+ break;
+
+ GtkWidget* item = GTK_WIDGET(child->data);
+ GtkRequisition itemRequisition;
+ gtk_widget_get_preferred_size(item, &itemRequisition, nullptr);
+ menuPosition.setY(menuPosition.y() - itemRequisition.height);
+ }
+ } else {
+ // Center vertically the empty popup in the combo box area.
+ menuPosition.setY(menuPosition.y() - rect.height() / 2);
+ }
+
+ gtk_menu_attach_to_widget(GTK_MENU(m_popup), GTK_WIDGET(m_webView), nullptr);
+
+ const GdkEvent* event = m_client->currentlyProcessedMouseDownEvent() ? m_client->currentlyProcessedMouseDownEvent()->nativeEvent() : nullptr;
+ gtk_menu_popup_for_device(GTK_MENU(m_popup), event ? gdk_event_get_device(event) : nullptr, nullptr, nullptr,
+ [](GtkMenu*, gint* x, gint* y, gboolean* pushIn, gpointer userData) {
+ // We can pass a pointer to the menuPosition local variable because the nested main loop ensures this is called in the function context.
+ IntPoint* menuPosition = static_cast<IntPoint*>(userData);
+ *x = menuPosition->x();
+ *y = menuPosition->y();
+ *pushIn = menuPosition->y() < 0;
+ }, &menuPosition, nullptr, event && event->type == GDK_BUTTON_PRESS ? event->button.button : 1,
+ event ? gdk_event_get_time(event) : GDK_CURRENT_TIME);
+
+ // Now that the menu has a position, schedule a resize to make sure it's resized to fit vertically in the work area.
+ gtk_widget_queue_resize(m_popup);
// PopupMenu can fail to open when there is no mouse grab.
// Ensure WebCore does not go into some pesky state.
- if (!gtk_widget_get_visible(m_popup->platformMenu())) {
+ if (!gtk_widget_get_visible(m_popup)) {
m_client->failedToShowPopupMenu();
return;
}
- // WebPageProxy expects the menu to run in a nested run loop, since it invalidates the
- // menu right after calling WebPopupMenuProxy::showPopupMenu().
- m_runLoop = adoptGRef(g_main_loop_new(0, FALSE));
-
- gdk_threads_leave();
- g_main_loop_run(m_runLoop.get());
- gdk_threads_enter();
-
- m_runLoop.clear();
+ // This ensures that the active item gets selected after popping up the menu, and
+ // as it says in "gtkcombobox.c" (line ~1606): it's ugly, but gets the job done.
+ GtkWidget* activeChild = gtk_menu_get_active(GTK_MENU(m_popup));
+ if (activeChild && gtk_widget_get_visible(activeChild))
+ gtk_menu_shell_select_item(GTK_MENU_SHELL(m_popup), activeChild);
+}
- g_signal_handler_disconnect(m_popup->platformMenu(), unmapHandler);
+void WebPopupMenuProxyGtk::hidePopupMenu()
+{
+ gtk_menu_popdown(GTK_MENU(m_popup));
+ resetTypeAheadFindState();
+}
- if (!m_client)
+void WebPopupMenuProxyGtk::cancelTracking()
+{
+ if (!m_popup)
return;
- m_client->valueChangedForPopupMenu(this, m_activeItem);
+ m_dismissMenuTimer.stop();
+ g_signal_handlers_disconnect_matched(m_popup, G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
+ hidePopupMenu();
+ gtk_widget_destroy(m_popup);
+ m_popup = nullptr;
}
-void WebPopupMenuProxyGtk::hidePopupMenu()
+bool WebPopupMenuProxyGtk::typeAheadFind(GdkEventKey* event)
{
- m_popup->popDown();
+ // 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;
+ GUniquePtr<gunichar2> utf16String(g_ucs4_to_utf16(&unicodeCharacter, 1, nullptr, &charactersWritten, nullptr));
+ 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.
+ static const uint32_t searchTimeoutMs = 1000;
+ bool repeatingCharacter = unicodeCharacter != m_previousKeyEventCharacter;
+ if (event->time - m_previousKeyEventTimestamp > searchTimeoutMs)
+ 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;
+
+ GUniquePtr<GList> children(gtk_container_get_children(GTK_CONTAINER(m_popup)));
+ if (!children)
+ return true;
+
+ // We case fold before searching, because strncmp does not handle non-ASCII characters.
+ GUniquePtr<gchar> searchStringWithCaseFolded(g_utf8_casefold(m_currentSearchString.utf8().data(), -1));
+ size_t prefixLength = strlen(searchStringWithCaseFolded.get());
+
+ // 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.get();
+ if (m_currentlySelectedMenuItem) {
+ currentChild = g_list_find(children.get(), m_currentlySelectedMenuItem);
+ if (!currentChild) {
+ m_currentlySelectedMenuItem = nullptr;
+ currentChild = children.get();
+ }
+
+ // 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.get();
+
+ GUniquePtr<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), GTK_WIDGET(currentChild->data));
+ break;
+ }
+ } while (currentChild != firstChild);
+
+ return true;
}
-void WebPopupMenuProxyGtk::shutdownRunLoop()
+void WebPopupMenuProxyGtk::resetTypeAheadFindState()
{
- if (g_main_loop_is_running(m_runLoop.get()))
- g_main_loop_quit(m_runLoop.get());
+ m_currentlySelectedMenuItem = nullptr;
+ m_previousKeyEventCharacter = 0;
+ m_previousKeyEventTimestamp = 0;
+ m_currentSearchString = emptyString();
}
void WebPopupMenuProxyGtk::menuItemActivated(GtkAction* action, WebPopupMenuProxyGtk* popupMenu)
{
- popupMenu->setActiveItem(GPOINTER_TO_INT(g_object_get_data(G_OBJECT(action), "popup-menu-action-index")));
- popupMenu->shutdownRunLoop();
+ popupMenu->m_dismissMenuTimer.stop();
+ if (popupMenu->m_client)
+ popupMenu->m_client->valueChangedForPopupMenu(popupMenu, GPOINTER_TO_INT(g_object_get_data(G_OBJECT(action), "popup-menu-action-index")));
+}
+
+void WebPopupMenuProxyGtk::dismissMenuTimerFired()
+{
+ if (m_client)
+ m_client->valueChangedForPopupMenu(this, -1);
+}
+
+void WebPopupMenuProxyGtk::menuUnmappedCallback(GtkWidget*, WebPopupMenuProxyGtk* popupMenu)
+{
+ if (!popupMenu->m_client)
+ return;
+
+ // When an item is activated, the menu is first hidden and then activate signal is emitted, so at this point we don't know
+ // if the menu has been hidden because an item has been selected or because the menu has been dismissed. Wait until the next
+ // main loop iteration to dismiss the menu, if an item is activated the timer will be cancelled.
+ popupMenu->m_dismissMenuTimer.startOneShot(0);
+}
+
+void WebPopupMenuProxyGtk::selectItemCallback(GtkWidget* item, WebPopupMenuProxyGtk* popupMenu)
+{
+ popupMenu->setCurrentlySelectedMenuItem(item);
}
-void WebPopupMenuProxyGtk::menuUnmapped(GtkWidget*, WebPopupMenuProxyGtk* popupMenu)
+gboolean WebPopupMenuProxyGtk::keyPressEventCallback(GtkWidget*, GdkEventKey* event, WebPopupMenuProxyGtk* popupMenu)
{
- popupMenu->shutdownRunLoop();
+ return popupMenu->typeAheadFind(event);
}
} // namespace WebKit