a6b1c9136a702c8335279d6d52fcf70b7dc660d9
[WebKit-https.git] / Source / WebKit / UIProcess / gtk / WebPopupMenuProxyGtk.cpp
1 /*
2  * Copyright (C) 2011 Igalia S.L.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "WebPopupMenuProxyGtk.h"
28
29 #include "NativeWebMouseEvent.h"
30 #include "WebPopupItem.h"
31 #include <WebCore/GtkUtilities.h>
32 #include <WebCore/IntRect.h>
33 #include <gtk/gtk.h>
34 #include <wtf/glib/GUniquePtr.h>
35 #include <wtf/text/CString.h>
36
37 namespace WebKit {
38 using namespace WebCore;
39
40 enum Columns {
41     Label,
42     Tooltip,
43     IsGroup,
44     IsSelected,
45     IsEnabled,
46     Index,
47
48     Count
49 };
50
51 WebPopupMenuProxyGtk::WebPopupMenuProxyGtk(GtkWidget* webView, WebPopupMenuProxy::Client& client)
52     : WebPopupMenuProxy(client)
53     , m_webView(webView)
54 {
55 }
56
57 WebPopupMenuProxyGtk::~WebPopupMenuProxyGtk()
58 {
59     cancelTracking();
60 }
61
62 void WebPopupMenuProxyGtk::selectItem(unsigned itemIndex)
63 {
64     if (m_client)
65         m_client->setTextFromItemForPopupMenu(this, itemIndex);
66     m_selectedItem = itemIndex;
67 }
68
69 void WebPopupMenuProxyGtk::activateItem(Optional<unsigned> itemIndex)
70 {
71     if (m_client)
72         m_client->valueChangedForPopupMenu(this, itemIndex.value_or(m_selectedItem.value_or(-1)));
73 }
74
75 bool WebPopupMenuProxyGtk::activateItemAtPath(GtkTreePath* path)
76 {
77     auto* model = gtk_tree_view_get_model(GTK_TREE_VIEW(m_treeView));
78     GtkTreeIter iter;
79     gtk_tree_model_get_iter(model, &iter, path);
80     gboolean isGroup, isEnabled;
81     guint index;
82     gtk_tree_model_get(model, &iter, Columns::IsGroup, &isGroup, Columns::IsEnabled, &isEnabled, Columns::Index, &index, -1);
83     if (isGroup || !isEnabled)
84         return false;
85
86     activateItem(index);
87     hidePopupMenu();
88     return true;
89 }
90
91 void WebPopupMenuProxyGtk::treeViewRowActivatedCallback(GtkTreeView*, GtkTreePath* path, GtkTreeViewColumn*, WebPopupMenuProxyGtk* popupMenu)
92 {
93     popupMenu->activateItemAtPath(path);
94 }
95
96 gboolean WebPopupMenuProxyGtk::treeViewButtonReleaseEventCallback(GtkWidget* treeView, GdkEventButton* event, WebPopupMenuProxyGtk* popupMenu)
97 {
98     if (event->button != GDK_BUTTON_PRIMARY)
99         return FALSE;
100
101     GUniqueOutPtr<GtkTreePath> path;
102     if (!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(treeView), event->x, event->y, &path.outPtr(), nullptr, nullptr, nullptr))
103         return FALSE;
104
105     return popupMenu->activateItemAtPath(path.get());
106 }
107
108 gboolean WebPopupMenuProxyGtk::buttonPressEventCallback(GtkWidget* widget, GdkEventButton* event, WebPopupMenuProxyGtk* popupMenu)
109 {
110     if (!popupMenu->m_device)
111         return FALSE;
112
113     popupMenu->hidePopupMenu();
114     return TRUE;
115 }
116
117 gboolean WebPopupMenuProxyGtk::keyPressEventCallback(GtkWidget* widget, GdkEventKey* event, WebPopupMenuProxyGtk* popupMenu)
118 {
119     if (!popupMenu->m_device)
120         return FALSE;
121
122     if (event->keyval == GDK_KEY_Escape) {
123         popupMenu->hidePopupMenu();
124         return TRUE;
125     }
126
127     if (popupMenu->typeAheadFind(event))
128         return TRUE;
129
130     // Forward the event to the tree view.
131     gtk_widget_event(popupMenu->m_treeView, reinterpret_cast<GdkEvent*>(event));
132     return TRUE;
133 }
134
135 void WebPopupMenuProxyGtk::createPopupMenu(const Vector<WebPopupItem>& items, int32_t selectedIndex)
136 {
137     ASSERT(!m_popup);
138
139     GRefPtr<GtkTreeStore> model = adoptGRef(gtk_tree_store_new(Columns::Count, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_UINT));
140     GtkTreeIter parentIter;
141     unsigned index = 0;
142     m_paths.reserveInitialCapacity(items.size());
143     for (const auto& item : items) {
144         if (item.m_isLabel) {
145             gtk_tree_store_insert_with_values(model.get(), &parentIter, nullptr, -1,
146                 Columns::Label, item.m_text.stripWhiteSpace().utf8().data(),
147                 Columns::IsGroup, TRUE,
148                 Columns::IsEnabled, TRUE,
149                 -1);
150             // We never need the path for group labels.
151             m_paths.uncheckedAppend(nullptr);
152         } else {
153             GtkTreeIter iter;
154             bool isSelected = selectedIndex && static_cast<unsigned>(selectedIndex) == index;
155             gtk_tree_store_insert_with_values(model.get(), &iter, item.m_text.startsWith("    ") ? &parentIter : nullptr, -1,
156                 Columns::Label, item.m_text.stripWhiteSpace().utf8().data(),
157                 Columns::Tooltip, item.m_toolTip.isEmpty() ? nullptr : item.m_toolTip.utf8().data(),
158                 Columns::IsGroup, FALSE,
159                 Columns::IsSelected, isSelected,
160                 Columns::IsEnabled, item.m_isEnabled,
161                 Columns::Index, index,
162                 -1);
163             if (isSelected) {
164                 ASSERT(!m_selectedItem);
165                 m_selectedItem = index;
166             }
167             m_paths.uncheckedAppend(GUniquePtr<GtkTreePath>(gtk_tree_model_get_path(GTK_TREE_MODEL(model.get()), &iter)));
168         }
169         index++;
170     }
171
172     m_treeView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model.get()));
173     auto* treeView = GTK_TREE_VIEW(m_treeView);
174     g_signal_connect(treeView, "row-activated", G_CALLBACK(treeViewRowActivatedCallback), this);
175     g_signal_connect_after(treeView, "button-release-event", G_CALLBACK(treeViewButtonReleaseEventCallback), this);
176     gtk_tree_view_set_tooltip_column(treeView, Columns::Tooltip);
177     gtk_tree_view_set_show_expanders(treeView, FALSE);
178     gtk_tree_view_set_level_indentation(treeView, 12);
179     gtk_tree_view_set_enable_search(treeView, FALSE);
180     gtk_tree_view_set_activate_on_single_click(treeView, TRUE);
181     gtk_tree_view_set_hover_selection(treeView, TRUE);
182     gtk_tree_view_set_headers_visible(treeView, FALSE);
183     gtk_tree_view_insert_column_with_data_func(treeView, 0, nullptr, gtk_cell_renderer_text_new(), [](GtkTreeViewColumn*, GtkCellRenderer* renderer, GtkTreeModel* model, GtkTreeIter* iter, gpointer) {
184         GUniqueOutPtr<char> label;
185         gboolean isGroup, isEnabled;
186         gtk_tree_model_get(model, iter, Columns::Label, &label.outPtr(), Columns::IsGroup, &isGroup, Columns::IsEnabled, &isEnabled, -1);
187         if (isGroup) {
188             GUniquePtr<char> markup(g_strdup_printf("<b>%s</b>", label.get()));
189             g_object_set(renderer, "markup", markup.get(), nullptr);
190         } else
191             g_object_set(renderer, "text", label.get(), "sensitive", isEnabled, nullptr);
192     }, nullptr, nullptr);
193     gtk_tree_view_expand_all(treeView);
194
195     auto* selection = gtk_tree_view_get_selection(treeView);
196     gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
197     gtk_tree_selection_unselect_all(selection);
198     gtk_tree_selection_set_select_function(selection, [](GtkTreeSelection*, GtkTreeModel* model, GtkTreePath* path, gboolean selected, gpointer) -> gboolean {
199         GtkTreeIter iter;
200         gtk_tree_model_get_iter(model, &iter, path);
201         gboolean isGroup, isEnabled;
202         gtk_tree_model_get(model, &iter, Columns::IsGroup, &isGroup, Columns::IsEnabled, &isEnabled, -1);
203         return !isGroup && isEnabled;
204     }, nullptr, nullptr);
205
206     auto* swindow = gtk_scrolled_window_new(nullptr, nullptr);
207     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swindow), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
208     gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swindow), GTK_SHADOW_ETCHED_IN);
209     gtk_container_add(GTK_CONTAINER(swindow), m_treeView);
210     gtk_widget_show(m_treeView);
211
212     m_popup = gtk_window_new(GTK_WINDOW_POPUP);
213     g_signal_connect(m_popup, "button-press-event", G_CALLBACK(buttonPressEventCallback), this);
214     g_signal_connect(m_popup, "key-press-event", G_CALLBACK(keyPressEventCallback), this);
215     gtk_window_set_type_hint(GTK_WINDOW(m_popup), GDK_WINDOW_TYPE_HINT_COMBO);
216     gtk_window_set_resizable(GTK_WINDOW(m_popup), FALSE);
217     gtk_container_add(GTK_CONTAINER(m_popup), swindow);
218     gtk_widget_show(swindow);
219 }
220
221 void WebPopupMenuProxyGtk::show()
222 {
223     if (m_selectedItem) {
224         auto& selectedPath = m_paths[m_selectedItem.value()];
225         ASSERT(selectedPath);
226         gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(m_treeView), selectedPath.get(), nullptr, TRUE, 0.5, 0);
227         gtk_tree_view_set_cursor(GTK_TREE_VIEW(m_treeView), selectedPath.get(), nullptr, FALSE);
228     }
229     gtk_widget_grab_focus(m_treeView);
230     gtk_widget_show(m_popup);
231 }
232
233 void WebPopupMenuProxyGtk::showPopupMenu(const IntRect& rect, TextDirection, double /* pageScaleFactor */, const Vector<WebPopupItem>& items, const PlatformPopupMenuData&, int32_t selectedIndex)
234 {
235     createPopupMenu(items, selectedIndex);
236     ASSERT(m_popup);
237
238     GtkRequisition treeViewRequisition;
239     gtk_widget_get_preferred_size(m_treeView, &treeViewRequisition, nullptr);
240     auto* column = gtk_tree_view_get_column(GTK_TREE_VIEW(m_treeView), Columns::Label);
241     gint itemHeight;
242     gtk_tree_view_column_cell_get_size(column, nullptr, nullptr, nullptr, nullptr, &itemHeight);
243     gint verticalSeparator;
244     gtk_widget_style_get(m_treeView, "vertical-separator", &verticalSeparator, nullptr);
245     itemHeight += verticalSeparator;
246     if (!itemHeight)
247         return;
248
249     auto* display = gtk_widget_get_display(m_webView);
250 #if GTK_CHECK_VERSION(3, 22, 0)
251     auto* monitor = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(m_webView));
252     GdkRectangle area;
253     gdk_monitor_get_workarea(monitor, &area);
254 #else
255     auto* screen = gtk_widget_get_screen(m_webView);
256     gint monitor = gdk_screen_get_monitor_at_window(screen, gtk_widget_get_window(m_webView));
257     GdkRectangle area;
258     gdk_screen_get_monitor_workarea(screen, monitor, &area);
259 #endif
260     int width = std::min(rect.width(), area.width);
261     size_t itemCount = std::min<size_t>(items.size(), (area.height / 3) / itemHeight);
262
263     auto* swindow = GTK_SCROLLED_WINDOW(gtk_bin_get_child(GTK_BIN(m_popup)));
264     // Disable scrollbars when there's only one item to ensure the scrolled window doesn't take them into account when calculating its minimum size.
265     gtk_scrolled_window_set_policy(swindow, GTK_POLICY_NEVER, itemCount > 1 ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER);
266     gtk_widget_realize(m_treeView);
267     gtk_tree_view_columns_autosize(GTK_TREE_VIEW(m_treeView));
268     gtk_scrolled_window_set_min_content_width(swindow, width);
269     gtk_widget_set_size_request(m_popup, width, -1);
270     gtk_scrolled_window_set_min_content_height(swindow, itemCount * itemHeight);
271
272     GtkRequisition menuRequisition;
273     gtk_widget_get_preferred_size(m_popup, &menuRequisition, nullptr);
274     IntPoint menuPosition = convertWidgetPointToScreenPoint(m_webView, rect.location());
275     // FIXME: We can't ensure the menu will be on screen in Wayland.
276     // https://blog.gtk.org/2016/07/15/future-of-relative-window-positioning/
277     // https://gitlab.gnome.org/GNOME/gtk/issues/997
278     if (menuPosition.x() + menuRequisition.width > area.x + area.width)
279         menuPosition.setX(area.x + area.width - menuRequisition.width);
280
281     if (menuPosition.y() + rect.height() + menuRequisition.height <= area.y + area.height
282         || menuPosition.y() - area.y < (area.y + area.height) - (menuPosition.y() + rect.height()))
283         menuPosition.move(0, rect.height());
284     else
285         menuPosition.move(0, -menuRequisition.height);
286     gtk_window_move(GTK_WINDOW(m_popup), menuPosition.x(), menuPosition.y());
287
288     auto* toplevel = gtk_widget_get_toplevel(m_webView);
289     if (GTK_IS_WINDOW(toplevel)) {
290         gtk_window_set_transient_for(GTK_WINDOW(m_popup), GTK_WINDOW(toplevel));
291         gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(m_popup));
292     }
293     gtk_window_set_attached_to(GTK_WINDOW(m_popup), m_webView);
294     gtk_window_set_screen(GTK_WINDOW(m_popup), gtk_widget_get_screen(m_webView));
295
296     const GdkEvent* event = m_client->currentlyProcessedMouseDownEvent() ? m_client->currentlyProcessedMouseDownEvent()->nativeEvent() : nullptr;
297     m_device = event ? gdk_event_get_device(event) : nullptr;
298     if (!m_device)
299         m_device = gtk_get_current_event_device();
300     if (m_device && gdk_device_get_display(m_device) != display)
301         m_device = nullptr;
302 #if GTK_CHECK_VERSION(3, 20, 0)
303     if (!m_device)
304         m_device = gdk_seat_get_pointer(gdk_display_get_default_seat(display));
305 #else
306     if (!m_device)
307         m_device = gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(display));
308 #endif
309     ASSERT(m_device);
310     if (gdk_device_get_source(m_device) == GDK_SOURCE_KEYBOARD)
311         m_device = gdk_device_get_associated_device(m_device);
312
313 #if GTK_CHECK_VERSION(3, 20, 0)
314     gtk_grab_add(m_popup);
315     auto grabResult = gdk_seat_grab(gdk_device_get_seat(m_device), gtk_widget_get_window(m_popup), GDK_SEAT_CAPABILITY_ALL, TRUE, nullptr, nullptr, [](GdkSeat*, GdkWindow*, gpointer userData) {
316         static_cast<WebPopupMenuProxyGtk*>(userData)->show();
317     }, this);
318 #else
319     gtk_device_grab_add(m_popup, m_device, TRUE);
320     auto grabResult = gdk_device_grab(m_device, gtk_widget_get_window(m_popup), GDK_OWNERSHIP_WINDOW, TRUE,
321         static_cast<GdkEventMask>(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK), nullptr, GDK_CURRENT_TIME);
322     show();
323 #endif
324
325     // PopupMenu can fail to open when there is no mouse grab.
326     // Ensure WebCore does not go into some pesky state.
327     if (grabResult != GDK_GRAB_SUCCESS) {
328        m_client->failedToShowPopupMenu();
329        return;
330     }
331 }
332
333 void WebPopupMenuProxyGtk::hidePopupMenu()
334 {
335     if (!m_popup)
336         return;
337
338     if (m_device) {
339 #if GTK_CHECK_VERSION(3, 20, 0)
340         gdk_seat_ungrab(gdk_device_get_seat(m_device));
341         gtk_grab_remove(m_popup);
342 #else
343         gdk_device_ungrab(m_device, GDK_CURRENT_TIME);
344         gtk_device_grab_remove(m_popup, m_device);
345 #endif
346         gtk_window_set_transient_for(GTK_WINDOW(m_popup), nullptr);
347         gtk_window_set_attached_to(GTK_WINDOW(m_popup), nullptr);
348         m_device = nullptr;
349     }
350
351     activateItem(WTF::nullopt);
352
353     if (m_currentSearchString) {
354         g_string_free(m_currentSearchString, TRUE);
355         m_currentSearchString = nullptr;
356     }
357
358     gtk_widget_destroy(m_popup);
359     m_popup = nullptr;
360 }
361
362 void WebPopupMenuProxyGtk::cancelTracking()
363 {
364     if (!m_popup)
365         return;
366
367     g_signal_handlers_disconnect_matched(m_popup, G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
368     hidePopupMenu();
369 }
370
371 Optional<unsigned> WebPopupMenuProxyGtk::typeAheadFindIndex(GdkEventKey* event)
372 {
373     gunichar keychar = gdk_keyval_to_unicode(event->keyval);
374     if (!g_unichar_isprint(keychar))
375         return WTF::nullopt;
376
377     if (event->time < m_previousKeyEventTime)
378         return WTF::nullopt;
379
380     static const uint32_t typeaheadTimeoutMs = 1000;
381     if (event->time - m_previousKeyEventTime > typeaheadTimeoutMs) {
382         if (m_currentSearchString)
383             g_string_truncate(m_currentSearchString, 0);
384     }
385     m_previousKeyEventTime = event->time;
386
387     if (!m_currentSearchString)
388         m_currentSearchString = g_string_new(nullptr);
389     g_string_append_unichar(m_currentSearchString, keychar);
390
391     int prefixLength = -1;
392     if (keychar == m_repeatingCharacter)
393         prefixLength = 1;
394     else
395         m_repeatingCharacter = m_currentSearchString->len == 1 ? keychar : '\0';
396
397     GtkTreeModel* model;
398     GtkTreeIter iter;
399     guint selectedIndex = 0;
400     if (gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(m_treeView)), &model, &iter))
401         gtk_tree_model_get(model, &iter, Columns::Index, &selectedIndex, -1);
402
403     unsigned index = selectedIndex;
404     if (m_repeatingCharacter != '\0')
405         index++;
406     auto itemCount = m_paths.size();
407     index %= itemCount;
408
409     GUniquePtr<char> normalizedPrefix(g_utf8_normalize(m_currentSearchString->str, prefixLength, G_NORMALIZE_ALL));
410     GUniquePtr<char> prefix(normalizedPrefix ? g_utf8_casefold(normalizedPrefix.get(), -1) : nullptr);
411     if (!prefix)
412         return WTF::nullopt;
413
414     model = gtk_tree_view_get_model(GTK_TREE_VIEW(m_treeView));
415     for (unsigned i = 0; i < itemCount; i++, index = (index + 1) % itemCount) {
416         auto& path = m_paths[index];
417         if (!path || !gtk_tree_model_get_iter(model, &iter, path.get()))
418             continue;
419
420         GUniqueOutPtr<char> label;
421         gboolean isEnabled;
422         gtk_tree_model_get(model, &iter, Columns::Label, &label.outPtr(), Columns::IsEnabled, &isEnabled, -1);
423         if (!isEnabled)
424             continue;
425
426         GUniquePtr<char> normalizedText(g_utf8_normalize(label.get(), -1, G_NORMALIZE_ALL));
427         GUniquePtr<char> text(normalizedText ? g_utf8_casefold(normalizedText.get(), -1) : nullptr);
428         if (!text)
429             continue;
430
431         if (!strncmp(prefix.get(), text.get(), strlen(prefix.get())))
432             return index;
433     }
434
435     return WTF::nullopt;
436 }
437
438 bool WebPopupMenuProxyGtk::typeAheadFind(GdkEventKey* event)
439 {
440     auto searchIndex = typeAheadFindIndex(event);
441     if (!searchIndex)
442         return false;
443
444     auto& path = m_paths[searchIndex.value()];
445     ASSERT(path);
446     gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, TRUE, 0.5, 0);
447     gtk_tree_view_set_cursor(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, FALSE);
448     selectItem(searchIndex.value());
449
450     return true;
451 }
452
453 } // namespace WebKit