afb86af2075f5ba2bd6970db3b3c494aef007939
[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.valueOr(m_selectedItem.valueOr(-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     guint button;
99     gdk_event_get_button(reinterpret_cast<GdkEvent*>(event), &button);
100     if (button != GDK_BUTTON_PRIMARY)
101         return FALSE;
102
103     double x, y;
104     gdk_event_get_coords(reinterpret_cast<GdkEvent*>(event), &x, &y);
105     GUniqueOutPtr<GtkTreePath> path;
106     if (!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(treeView), x, y, &path.outPtr(), nullptr, nullptr, nullptr))
107         return FALSE;
108
109     return popupMenu->activateItemAtPath(path.get());
110 }
111
112 gboolean WebPopupMenuProxyGtk::buttonPressEventCallback(GtkWidget* widget, GdkEventButton* event, WebPopupMenuProxyGtk* popupMenu)
113 {
114     if (!popupMenu->m_device)
115         return FALSE;
116
117     popupMenu->hidePopupMenu();
118     return TRUE;
119 }
120
121 gboolean WebPopupMenuProxyGtk::keyPressEventCallback(GtkWidget* widget, GdkEventKey* event, WebPopupMenuProxyGtk* popupMenu)
122 {
123     if (!popupMenu->m_device)
124         return FALSE;
125
126     guint keyval;
127     gdk_event_get_keyval(reinterpret_cast<GdkEvent*>(event), &keyval);
128     if (keyval == GDK_KEY_Escape) {
129         popupMenu->hidePopupMenu();
130         return TRUE;
131     }
132
133     if (popupMenu->typeAheadFind(event))
134         return TRUE;
135
136     // Forward the event to the tree view.
137     gtk_widget_event(popupMenu->m_treeView, reinterpret_cast<GdkEvent*>(event));
138     return TRUE;
139 }
140
141 void WebPopupMenuProxyGtk::createPopupMenu(const Vector<WebPopupItem>& items, int32_t selectedIndex)
142 {
143     ASSERT(!m_popup);
144
145     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));
146     GtkTreeIter parentIter;
147     unsigned index = 0;
148     m_paths.reserveInitialCapacity(items.size());
149     for (const auto& item : items) {
150         if (item.m_isLabel) {
151             gtk_tree_store_insert_with_values(model.get(), &parentIter, nullptr, -1,
152                 Columns::Label, item.m_text.stripWhiteSpace().utf8().data(),
153                 Columns::IsGroup, TRUE,
154                 Columns::IsEnabled, TRUE,
155                 -1);
156             // We never need the path for group labels.
157             m_paths.uncheckedAppend(nullptr);
158         } else {
159             GtkTreeIter iter;
160             bool isSelected = selectedIndex && static_cast<unsigned>(selectedIndex) == index;
161             gtk_tree_store_insert_with_values(model.get(), &iter, item.m_text.startsWith("    ") ? &parentIter : nullptr, -1,
162                 Columns::Label, item.m_text.stripWhiteSpace().utf8().data(),
163                 Columns::Tooltip, item.m_toolTip.isEmpty() ? nullptr : item.m_toolTip.utf8().data(),
164                 Columns::IsGroup, FALSE,
165                 Columns::IsSelected, isSelected,
166                 Columns::IsEnabled, item.m_isEnabled,
167                 Columns::Index, index,
168                 -1);
169             if (isSelected) {
170                 ASSERT(!m_selectedItem);
171                 m_selectedItem = index;
172             }
173             m_paths.uncheckedAppend(GUniquePtr<GtkTreePath>(gtk_tree_model_get_path(GTK_TREE_MODEL(model.get()), &iter)));
174         }
175         index++;
176     }
177
178     m_treeView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model.get()));
179     auto* treeView = GTK_TREE_VIEW(m_treeView);
180     g_signal_connect(treeView, "row-activated", G_CALLBACK(treeViewRowActivatedCallback), this);
181     g_signal_connect_after(treeView, "button-release-event", G_CALLBACK(treeViewButtonReleaseEventCallback), this);
182     gtk_tree_view_set_tooltip_column(treeView, Columns::Tooltip);
183     gtk_tree_view_set_show_expanders(treeView, FALSE);
184     gtk_tree_view_set_level_indentation(treeView, 12);
185     gtk_tree_view_set_enable_search(treeView, FALSE);
186     gtk_tree_view_set_activate_on_single_click(treeView, TRUE);
187     gtk_tree_view_set_hover_selection(treeView, TRUE);
188     gtk_tree_view_set_headers_visible(treeView, FALSE);
189     gtk_tree_view_insert_column_with_data_func(treeView, 0, nullptr, gtk_cell_renderer_text_new(), [](GtkTreeViewColumn*, GtkCellRenderer* renderer, GtkTreeModel* model, GtkTreeIter* iter, gpointer) {
190         GUniqueOutPtr<char> label;
191         gboolean isGroup, isEnabled;
192         gtk_tree_model_get(model, iter, Columns::Label, &label.outPtr(), Columns::IsGroup, &isGroup, Columns::IsEnabled, &isEnabled, -1);
193         if (isGroup) {
194             GUniquePtr<char> markup(g_strdup_printf("<b>%s</b>", label.get()));
195             g_object_set(renderer, "markup", markup.get(), nullptr);
196         } else
197             g_object_set(renderer, "text", label.get(), "sensitive", isEnabled, nullptr);
198     }, nullptr, nullptr);
199     gtk_tree_view_expand_all(treeView);
200
201     auto* selection = gtk_tree_view_get_selection(treeView);
202     gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
203     gtk_tree_selection_unselect_all(selection);
204     gtk_tree_selection_set_select_function(selection, [](GtkTreeSelection*, GtkTreeModel* model, GtkTreePath* path, gboolean selected, gpointer) -> gboolean {
205         GtkTreeIter iter;
206         gtk_tree_model_get_iter(model, &iter, path);
207         gboolean isGroup, isEnabled;
208         gtk_tree_model_get(model, &iter, Columns::IsGroup, &isGroup, Columns::IsEnabled, &isEnabled, -1);
209         return !isGroup && isEnabled;
210     }, nullptr, nullptr);
211
212     auto* swindow = gtk_scrolled_window_new(nullptr, nullptr);
213     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swindow), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
214     gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swindow), GTK_SHADOW_ETCHED_IN);
215     gtk_container_add(GTK_CONTAINER(swindow), m_treeView);
216     gtk_widget_show(m_treeView);
217
218     m_popup = gtk_window_new(GTK_WINDOW_POPUP);
219     g_signal_connect(m_popup, "button-press-event", G_CALLBACK(buttonPressEventCallback), this);
220     g_signal_connect(m_popup, "key-press-event", G_CALLBACK(keyPressEventCallback), this);
221     gtk_window_set_type_hint(GTK_WINDOW(m_popup), GDK_WINDOW_TYPE_HINT_COMBO);
222     gtk_window_set_resizable(GTK_WINDOW(m_popup), FALSE);
223     gtk_container_add(GTK_CONTAINER(m_popup), swindow);
224     gtk_widget_show(swindow);
225 }
226
227 void WebPopupMenuProxyGtk::show()
228 {
229     if (m_selectedItem) {
230         auto& selectedPath = m_paths[m_selectedItem.value()];
231         ASSERT(selectedPath);
232         gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(m_treeView), selectedPath.get(), nullptr, TRUE, 0.5, 0);
233         gtk_tree_view_set_cursor(GTK_TREE_VIEW(m_treeView), selectedPath.get(), nullptr, FALSE);
234     }
235     gtk_widget_grab_focus(m_treeView);
236     gtk_widget_show(m_popup);
237 }
238
239 void WebPopupMenuProxyGtk::showPopupMenu(const IntRect& rect, TextDirection, double /* pageScaleFactor */, const Vector<WebPopupItem>& items, const PlatformPopupMenuData&, int32_t selectedIndex)
240 {
241     createPopupMenu(items, selectedIndex);
242     ASSERT(m_popup);
243
244     GtkRequisition treeViewRequisition;
245     gtk_widget_get_preferred_size(m_treeView, &treeViewRequisition, nullptr);
246     auto* column = gtk_tree_view_get_column(GTK_TREE_VIEW(m_treeView), Columns::Label);
247     gint itemHeight;
248     gtk_tree_view_column_cell_get_size(column, nullptr, nullptr, nullptr, nullptr, &itemHeight);
249     gint verticalSeparator;
250     gtk_widget_style_get(m_treeView, "vertical-separator", &verticalSeparator, nullptr);
251     itemHeight += verticalSeparator;
252     if (!itemHeight)
253         return;
254
255     auto* display = gtk_widget_get_display(m_webView);
256 #if GTK_CHECK_VERSION(3, 22, 0)
257     auto* monitor = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(m_webView));
258     GdkRectangle area;
259     gdk_monitor_get_workarea(monitor, &area);
260 #else
261     auto* screen = gtk_widget_get_screen(m_webView);
262     gint monitor = gdk_screen_get_monitor_at_window(screen, gtk_widget_get_window(m_webView));
263     GdkRectangle area;
264     gdk_screen_get_monitor_workarea(screen, monitor, &area);
265 #endif
266     int width = std::min(rect.width(), area.width);
267     size_t itemCount = std::min<size_t>(items.size(), (area.height / 3) / itemHeight);
268
269     auto* swindow = GTK_SCROLLED_WINDOW(gtk_bin_get_child(GTK_BIN(m_popup)));
270     // Disable scrollbars when there's only one item to ensure the scrolled window doesn't take them into account when calculating its minimum size.
271     gtk_scrolled_window_set_policy(swindow, GTK_POLICY_NEVER, itemCount > 1 ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER);
272     gtk_widget_realize(m_treeView);
273     gtk_tree_view_columns_autosize(GTK_TREE_VIEW(m_treeView));
274     gtk_scrolled_window_set_min_content_width(swindow, width);
275     gtk_widget_set_size_request(m_popup, width, -1);
276     gtk_scrolled_window_set_min_content_height(swindow, itemCount * itemHeight);
277
278     GtkRequisition menuRequisition;
279     gtk_widget_get_preferred_size(m_popup, &menuRequisition, nullptr);
280     IntPoint menuPosition = convertWidgetPointToScreenPoint(m_webView, rect.location());
281     // FIXME: We can't ensure the menu will be on screen in Wayland.
282     // https://blog.gtk.org/2016/07/15/future-of-relative-window-positioning/
283     // https://gitlab.gnome.org/GNOME/gtk/issues/997
284     if (menuPosition.x() + menuRequisition.width > area.x + area.width)
285         menuPosition.setX(area.x + area.width - menuRequisition.width);
286
287     if (menuPosition.y() + rect.height() + menuRequisition.height <= area.y + area.height
288         || menuPosition.y() - area.y < (area.y + area.height) - (menuPosition.y() + rect.height()))
289         menuPosition.move(0, rect.height());
290     else
291         menuPosition.move(0, -menuRequisition.height);
292     gtk_window_move(GTK_WINDOW(m_popup), menuPosition.x(), menuPosition.y());
293
294     auto* toplevel = gtk_widget_get_toplevel(m_webView);
295     if (GTK_IS_WINDOW(toplevel)) {
296         gtk_window_set_transient_for(GTK_WINDOW(m_popup), GTK_WINDOW(toplevel));
297         gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(m_popup));
298     }
299     gtk_window_set_attached_to(GTK_WINDOW(m_popup), m_webView);
300     gtk_window_set_screen(GTK_WINDOW(m_popup), gtk_widget_get_screen(m_webView));
301
302     const GdkEvent* event = m_client->currentlyProcessedMouseDownEvent() ? m_client->currentlyProcessedMouseDownEvent()->nativeEvent() : nullptr;
303     m_device = event ? gdk_event_get_device(event) : nullptr;
304     if (!m_device)
305         m_device = gtk_get_current_event_device();
306     if (m_device && gdk_device_get_display(m_device) != display)
307         m_device = nullptr;
308 #if GTK_CHECK_VERSION(3, 20, 0)
309     if (!m_device)
310         m_device = gdk_seat_get_pointer(gdk_display_get_default_seat(display));
311 #else
312     if (!m_device)
313         m_device = gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(display));
314 #endif
315     ASSERT(m_device);
316     if (gdk_device_get_source(m_device) == GDK_SOURCE_KEYBOARD)
317         m_device = gdk_device_get_associated_device(m_device);
318
319 #if GTK_CHECK_VERSION(3, 20, 0)
320     gtk_grab_add(m_popup);
321     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) {
322         static_cast<WebPopupMenuProxyGtk*>(userData)->show();
323     }, this);
324 #else
325     gtk_device_grab_add(m_popup, m_device, TRUE);
326     auto grabResult = gdk_device_grab(m_device, gtk_widget_get_window(m_popup), GDK_OWNERSHIP_WINDOW, TRUE,
327         static_cast<GdkEventMask>(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK), nullptr, GDK_CURRENT_TIME);
328     show();
329 #endif
330
331     // PopupMenu can fail to open when there is no mouse grab.
332     // Ensure WebCore does not go into some pesky state.
333     if (grabResult != GDK_GRAB_SUCCESS) {
334        m_client->failedToShowPopupMenu();
335        return;
336     }
337 }
338
339 void WebPopupMenuProxyGtk::hidePopupMenu()
340 {
341     if (!m_popup)
342         return;
343
344     if (m_device) {
345 #if GTK_CHECK_VERSION(3, 20, 0)
346         gdk_seat_ungrab(gdk_device_get_seat(m_device));
347         gtk_grab_remove(m_popup);
348 #else
349         gdk_device_ungrab(m_device, GDK_CURRENT_TIME);
350         gtk_device_grab_remove(m_popup, m_device);
351 #endif
352         gtk_window_set_transient_for(GTK_WINDOW(m_popup), nullptr);
353         gtk_window_set_attached_to(GTK_WINDOW(m_popup), nullptr);
354         m_device = nullptr;
355     }
356
357     activateItem(WTF::nullopt);
358
359     if (m_currentSearchString) {
360         g_string_free(m_currentSearchString, TRUE);
361         m_currentSearchString = nullptr;
362     }
363
364     gtk_widget_destroy(m_popup);
365     m_popup = nullptr;
366 }
367
368 void WebPopupMenuProxyGtk::cancelTracking()
369 {
370     if (!m_popup)
371         return;
372
373     g_signal_handlers_disconnect_matched(m_popup, G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
374     hidePopupMenu();
375 }
376
377 Optional<unsigned> WebPopupMenuProxyGtk::typeAheadFindIndex(GdkEventKey* event)
378 {
379     guint keyval;
380     gdk_event_get_keyval(reinterpret_cast<GdkEvent*>(event), &keyval);
381     gunichar keychar = gdk_keyval_to_unicode(keyval);
382     if (!g_unichar_isprint(keychar))
383         return WTF::nullopt;
384
385     uint32_t time = gdk_event_get_time(reinterpret_cast<GdkEvent*>(event));
386     if (time < m_previousKeyEventTime)
387         return WTF::nullopt;
388
389     static const uint32_t typeaheadTimeoutMs = 1000;
390     if (time - m_previousKeyEventTime > typeaheadTimeoutMs) {
391         if (m_currentSearchString)
392             g_string_truncate(m_currentSearchString, 0);
393     }
394     m_previousKeyEventTime = time;
395
396     if (!m_currentSearchString)
397         m_currentSearchString = g_string_new(nullptr);
398     g_string_append_unichar(m_currentSearchString, keychar);
399
400     int prefixLength = -1;
401     if (keychar == m_repeatingCharacter)
402         prefixLength = 1;
403     else
404         m_repeatingCharacter = m_currentSearchString->len == 1 ? keychar : '\0';
405
406     GtkTreeModel* model;
407     GtkTreeIter iter;
408     guint selectedIndex = 0;
409     if (gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(m_treeView)), &model, &iter))
410         gtk_tree_model_get(model, &iter, Columns::Index, &selectedIndex, -1);
411
412     unsigned index = selectedIndex;
413     if (m_repeatingCharacter != '\0')
414         index++;
415     auto itemCount = m_paths.size();
416     index %= itemCount;
417
418     GUniquePtr<char> normalizedPrefix(g_utf8_normalize(m_currentSearchString->str, prefixLength, G_NORMALIZE_ALL));
419     GUniquePtr<char> prefix(normalizedPrefix ? g_utf8_casefold(normalizedPrefix.get(), -1) : nullptr);
420     if (!prefix)
421         return WTF::nullopt;
422
423     model = gtk_tree_view_get_model(GTK_TREE_VIEW(m_treeView));
424     for (unsigned i = 0; i < itemCount; i++, index = (index + 1) % itemCount) {
425         auto& path = m_paths[index];
426         if (!path || !gtk_tree_model_get_iter(model, &iter, path.get()))
427             continue;
428
429         GUniqueOutPtr<char> label;
430         gboolean isEnabled;
431         gtk_tree_model_get(model, &iter, Columns::Label, &label.outPtr(), Columns::IsEnabled, &isEnabled, -1);
432         if (!isEnabled)
433             continue;
434
435         GUniquePtr<char> normalizedText(g_utf8_normalize(label.get(), -1, G_NORMALIZE_ALL));
436         GUniquePtr<char> text(normalizedText ? g_utf8_casefold(normalizedText.get(), -1) : nullptr);
437         if (!text)
438             continue;
439
440         if (!strncmp(prefix.get(), text.get(), strlen(prefix.get())))
441             return index;
442     }
443
444     return WTF::nullopt;
445 }
446
447 bool WebPopupMenuProxyGtk::typeAheadFind(GdkEventKey* event)
448 {
449     auto searchIndex = typeAheadFindIndex(event);
450     if (!searchIndex)
451         return false;
452
453     auto& path = m_paths[searchIndex.value()];
454     ASSERT(path);
455     gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, TRUE, 0.5, 0);
456     gtk_tree_view_set_cursor(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, FALSE);
457     selectItem(searchIndex.value());
458
459     return true;
460 }
461
462 } // namespace WebKit