[WPE][GTK] Bump minimum versions of GLib, GTK, libsoup, ATK, GStreamer, and Cairo
[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     auto* monitor = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(m_webView));
257     GdkRectangle area;
258     gdk_monitor_get_workarea(monitor, &area);
259     int width = std::min(rect.width(), area.width);
260     size_t itemCount = std::min<size_t>(items.size(), (area.height / 3) / itemHeight);
261
262     auto* swindow = GTK_SCROLLED_WINDOW(gtk_bin_get_child(GTK_BIN(m_popup)));
263     // Disable scrollbars when there's only one item to ensure the scrolled window doesn't take them into account when calculating its minimum size.
264     gtk_scrolled_window_set_policy(swindow, GTK_POLICY_NEVER, itemCount > 1 ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER);
265     gtk_widget_realize(m_treeView);
266     gtk_tree_view_columns_autosize(GTK_TREE_VIEW(m_treeView));
267     gtk_scrolled_window_set_min_content_width(swindow, width);
268     gtk_widget_set_size_request(m_popup, width, -1);
269     gtk_scrolled_window_set_min_content_height(swindow, itemCount * itemHeight);
270
271     GtkRequisition menuRequisition;
272     gtk_widget_get_preferred_size(m_popup, &menuRequisition, nullptr);
273     IntPoint menuPosition = convertWidgetPointToScreenPoint(m_webView, rect.location());
274     // FIXME: We can't ensure the menu will be on screen in Wayland.
275     // https://blog.gtk.org/2016/07/15/future-of-relative-window-positioning/
276     // https://gitlab.gnome.org/GNOME/gtk/issues/997
277     if (menuPosition.x() + menuRequisition.width > area.x + area.width)
278         menuPosition.setX(area.x + area.width - menuRequisition.width);
279
280     if (menuPosition.y() + rect.height() + menuRequisition.height <= area.y + area.height
281         || menuPosition.y() - area.y < (area.y + area.height) - (menuPosition.y() + rect.height()))
282         menuPosition.move(0, rect.height());
283     else
284         menuPosition.move(0, -menuRequisition.height);
285     gtk_window_move(GTK_WINDOW(m_popup), menuPosition.x(), menuPosition.y());
286
287     auto* toplevel = gtk_widget_get_toplevel(m_webView);
288     if (GTK_IS_WINDOW(toplevel)) {
289         gtk_window_set_transient_for(GTK_WINDOW(m_popup), GTK_WINDOW(toplevel));
290         gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(m_popup));
291     }
292     gtk_window_set_attached_to(GTK_WINDOW(m_popup), m_webView);
293     gtk_window_set_screen(GTK_WINDOW(m_popup), gtk_widget_get_screen(m_webView));
294
295     const GdkEvent* event = m_client->currentlyProcessedMouseDownEvent() ? m_client->currentlyProcessedMouseDownEvent()->nativeEvent() : nullptr;
296     m_device = event ? gdk_event_get_device(event) : nullptr;
297     if (!m_device)
298         m_device = gtk_get_current_event_device();
299     if (m_device && gdk_device_get_display(m_device) != display)
300         m_device = nullptr;
301     if (!m_device)
302         m_device = gdk_seat_get_pointer(gdk_display_get_default_seat(display));
303     ASSERT(m_device);
304     if (gdk_device_get_source(m_device) == GDK_SOURCE_KEYBOARD)
305         m_device = gdk_device_get_associated_device(m_device);
306
307     gtk_grab_add(m_popup);
308     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) {
309         static_cast<WebPopupMenuProxyGtk*>(userData)->show();
310     }, this);
311
312     // PopupMenu can fail to open when there is no mouse grab.
313     // Ensure WebCore does not go into some pesky state.
314     if (grabResult != GDK_GRAB_SUCCESS) {
315        m_client->failedToShowPopupMenu();
316        return;
317     }
318 }
319
320 void WebPopupMenuProxyGtk::hidePopupMenu()
321 {
322     if (!m_popup)
323         return;
324
325     if (m_device) {
326         gdk_seat_ungrab(gdk_device_get_seat(m_device));
327         gtk_grab_remove(m_popup);
328         gtk_window_set_transient_for(GTK_WINDOW(m_popup), nullptr);
329         gtk_window_set_attached_to(GTK_WINDOW(m_popup), nullptr);
330         m_device = nullptr;
331     }
332
333     activateItem(WTF::nullopt);
334
335     if (m_currentSearchString) {
336         g_string_free(m_currentSearchString, TRUE);
337         m_currentSearchString = nullptr;
338     }
339
340     gtk_widget_destroy(m_popup);
341     m_popup = nullptr;
342 }
343
344 void WebPopupMenuProxyGtk::cancelTracking()
345 {
346     if (!m_popup)
347         return;
348
349     g_signal_handlers_disconnect_matched(m_popup, G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
350     hidePopupMenu();
351 }
352
353 Optional<unsigned> WebPopupMenuProxyGtk::typeAheadFindIndex(GdkEventKey* event)
354 {
355     guint keyval;
356     gdk_event_get_keyval(reinterpret_cast<GdkEvent*>(event), &keyval);
357     gunichar keychar = gdk_keyval_to_unicode(keyval);
358     if (!g_unichar_isprint(keychar))
359         return WTF::nullopt;
360
361     uint32_t time = gdk_event_get_time(reinterpret_cast<GdkEvent*>(event));
362     if (time < m_previousKeyEventTime)
363         return WTF::nullopt;
364
365     static const uint32_t typeaheadTimeoutMs = 1000;
366     if (time - m_previousKeyEventTime > typeaheadTimeoutMs) {
367         if (m_currentSearchString)
368             g_string_truncate(m_currentSearchString, 0);
369     }
370     m_previousKeyEventTime = time;
371
372     if (!m_currentSearchString)
373         m_currentSearchString = g_string_new(nullptr);
374     g_string_append_unichar(m_currentSearchString, keychar);
375
376     int prefixLength = -1;
377     if (keychar == m_repeatingCharacter)
378         prefixLength = 1;
379     else
380         m_repeatingCharacter = m_currentSearchString->len == 1 ? keychar : '\0';
381
382     GtkTreeModel* model;
383     GtkTreeIter iter;
384     guint selectedIndex = 0;
385     if (gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(m_treeView)), &model, &iter))
386         gtk_tree_model_get(model, &iter, Columns::Index, &selectedIndex, -1);
387
388     unsigned index = selectedIndex;
389     if (m_repeatingCharacter != '\0')
390         index++;
391     auto itemCount = m_paths.size();
392     index %= itemCount;
393
394     GUniquePtr<char> normalizedPrefix(g_utf8_normalize(m_currentSearchString->str, prefixLength, G_NORMALIZE_ALL));
395     GUniquePtr<char> prefix(normalizedPrefix ? g_utf8_casefold(normalizedPrefix.get(), -1) : nullptr);
396     if (!prefix)
397         return WTF::nullopt;
398
399     model = gtk_tree_view_get_model(GTK_TREE_VIEW(m_treeView));
400     for (unsigned i = 0; i < itemCount; i++, index = (index + 1) % itemCount) {
401         auto& path = m_paths[index];
402         if (!path || !gtk_tree_model_get_iter(model, &iter, path.get()))
403             continue;
404
405         GUniqueOutPtr<char> label;
406         gboolean isEnabled;
407         gtk_tree_model_get(model, &iter, Columns::Label, &label.outPtr(), Columns::IsEnabled, &isEnabled, -1);
408         if (!isEnabled)
409             continue;
410
411         GUniquePtr<char> normalizedText(g_utf8_normalize(label.get(), -1, G_NORMALIZE_ALL));
412         GUniquePtr<char> text(normalizedText ? g_utf8_casefold(normalizedText.get(), -1) : nullptr);
413         if (!text)
414             continue;
415
416         if (!strncmp(prefix.get(), text.get(), strlen(prefix.get())))
417             return index;
418     }
419
420     return WTF::nullopt;
421 }
422
423 bool WebPopupMenuProxyGtk::typeAheadFind(GdkEventKey* event)
424 {
425     auto searchIndex = typeAheadFindIndex(event);
426     if (!searchIndex)
427         return false;
428
429     auto& path = m_paths[searchIndex.value()];
430     ASSERT(path);
431     gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, TRUE, 0.5, 0);
432     gtk_tree_view_set_cursor(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, FALSE);
433     selectItem(searchIndex.value());
434
435     return true;
436 }
437
438 } // namespace WebKit