1/*
2 * Copyright (C) 2019 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 "WebDataListSuggestionsDropdownGtk.h"
28
29#if ENABLE(DATALIST_ELEMENT)
30
31#include <WebCore/DataListSuggestionInformation.h>
32#include <wtf/glib/GRefPtr.h>
33#include <wtf/glib/GUniquePtr.h>
34
35namespace WebKit {
36
37static void firstTimeItemSelectedCallback(GtkTreeSelection* selection, GtkWidget* treeView)
38{
39 if (gtk_widget_is_focus(treeView))
40 gtk_tree_selection_unselect_all(selection);
41 g_signal_handlers_disconnect_by_func(selection, reinterpret_cast<gpointer>(firstTimeItemSelectedCallback), treeView);
42}
43
44WebDataListSuggestionsDropdownGtk::WebDataListSuggestionsDropdownGtk(GtkWidget* webView, WebPageProxy& page)
45 : WebDataListSuggestionsDropdown(page)
46 , m_webView(webView)
47{
48 GRefPtr<GtkListStore> model = adoptGRef(gtk_list_store_new(1, G_TYPE_STRING));
49 m_treeView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model.get()));
50 auto* treeView = GTK_TREE_VIEW(m_treeView);
51 g_signal_connect(treeView, "row-activated", G_CALLBACK(treeViewRowActivatedCallback), this);
52 gtk_tree_view_set_enable_search(treeView, FALSE);
53 gtk_tree_view_set_activate_on_single_click(treeView, TRUE);
54 gtk_tree_view_set_hover_selection(treeView, TRUE);
55 gtk_tree_view_set_headers_visible(treeView, FALSE);
56 gtk_tree_view_insert_column_with_attributes(treeView, 0, nullptr, gtk_cell_renderer_text_new(), "text", 0, nullptr);
57
58 auto* selection = gtk_tree_view_get_selection(treeView);
59 // The first time it's shown the first item is always selected, so we connect to selection changed to unselect it.
60 g_signal_connect_object(selection, "changed", G_CALLBACK(firstTimeItemSelectedCallback), treeView, static_cast<GConnectFlags>(0));
61 gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
62
63 auto* swindow = gtk_scrolled_window_new(nullptr, nullptr);
64 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swindow), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
65 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swindow), GTK_SHADOW_ETCHED_IN);
66 gtk_container_add(GTK_CONTAINER(swindow), m_treeView);
67 gtk_widget_show(m_treeView);
68
69 m_popup = gtk_window_new(GTK_WINDOW_POPUP);
70 gtk_window_set_type_hint(GTK_WINDOW(m_popup), GDK_WINDOW_TYPE_HINT_COMBO);
71 gtk_window_set_resizable(GTK_WINDOW(m_popup), FALSE);
72 gtk_container_add(GTK_CONTAINER(m_popup), swindow);
73 gtk_widget_show(swindow);
74
75 g_signal_connect_object(m_webView, "focus-out-event", G_CALLBACK(gtk_widget_hide), m_popup, G_CONNECT_SWAPPED);
76 g_signal_connect_object(m_webView, "unmap-event", G_CALLBACK(gtk_widget_hide), m_popup, G_CONNECT_SWAPPED);
77
78#if ENABLE(DEVELOPER_MODE)
79 g_object_set_data(G_OBJECT(m_webView), "wk-datalist-popup", m_popup);
80#endif
81}
82
83WebDataListSuggestionsDropdownGtk::~WebDataListSuggestionsDropdownGtk()
84{
85 gtk_window_set_transient_for(GTK_WINDOW(m_popup), nullptr);
86 gtk_window_set_attached_to(GTK_WINDOW(m_popup), nullptr);
87#if ENABLE(DEVELOPER_MODE)
88 g_object_set_data(G_OBJECT(m_webView), "wk-datalist-popup", nullptr);
89#endif
90 gtk_widget_destroy(m_popup);
91}
92
93void WebDataListSuggestionsDropdownGtk::treeViewRowActivatedCallback(GtkTreeView* treeView, GtkTreePath* path, GtkTreeViewColumn*, WebDataListSuggestionsDropdownGtk* menu)
94{
95 auto* model = gtk_tree_view_get_model(treeView);
96 GtkTreeIter iter;
97 gtk_tree_model_get_iter(model, &iter, path);
98 GUniqueOutPtr<char> item;
99 gtk_tree_model_get(model, &iter, 0, &item.outPtr(), -1);
100
101 menu->didSelectOption(String::fromUTF8(item.get()));
102}
103
104void WebDataListSuggestionsDropdownGtk::didSelectOption(const String& selectedOption)
105{
106 if (!m_page)
107 return;
108
109 m_page->didSelectOption(selectedOption);
110 close();
111}
112
113void WebDataListSuggestionsDropdownGtk::show(WebCore::DataListSuggestionInformation&& information)
114{
115 auto* model = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(m_treeView)));
116 gtk_list_store_clear(model);
117 for (const auto& suggestion : information.suggestions) {
118 GtkTreeIter iter;
119 gtk_list_store_append(model, &iter);
120 gtk_list_store_set(model, &iter, 0, suggestion.utf8().data(), -1);
121 }
122
123 GtkRequisition treeViewRequisition;
124 gtk_widget_get_preferred_size(m_treeView, &treeViewRequisition, nullptr);
125 auto* column = gtk_tree_view_get_column(GTK_TREE_VIEW(m_treeView), 0);
126 gint itemHeight;
127 gtk_tree_view_column_cell_get_size(column, nullptr, nullptr, nullptr, nullptr, &itemHeight);
128 gint verticalSeparator;
129 gtk_widget_style_get(m_treeView, "vertical-separator", &verticalSeparator, nullptr);
130 itemHeight += verticalSeparator;
131 if (!itemHeight)
132 return;
133
134 auto* display = gtk_widget_get_display(m_webView);
135 auto* monitor = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(m_webView));
136 GdkRectangle area;
137 gdk_monitor_get_workarea(monitor, &area);
138 int width = std::min(information.elementRect.width(), area.width);
139 size_t itemCount = std::min<size_t>(information.suggestions.size(), (area.height / 3) / itemHeight);
140
141 auto* swindow = GTK_SCROLLED_WINDOW(gtk_bin_get_child(GTK_BIN(m_popup)));
142 // Disable scrollbars when there's only one item to ensure the scrolled window doesn't take them into account when calculating its minimum size.
143 gtk_scrolled_window_set_policy(swindow, GTK_POLICY_NEVER, itemCount > 1 ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER);
144 gtk_widget_realize(m_treeView);
145 gtk_tree_view_columns_autosize(GTK_TREE_VIEW(m_treeView));
146 gtk_scrolled_window_set_min_content_width(swindow, width);
147 gtk_widget_set_size_request(m_popup, width, -1);
148 gtk_scrolled_window_set_min_content_height(swindow, itemCount * itemHeight);
149
150 GtkRequisition menuRequisition;
151 gtk_widget_get_preferred_size(m_popup, &menuRequisition, nullptr);
152 IntPoint menuPosition = convertWidgetPointToScreenPoint(m_webView, information.elementRect.location());
153 // FIXME: We can't ensure the menu will be on screen in Wayland.
154 // https://blog.gtk.org/2016/07/15/future-of-relative-window-positioning/
155 // https://gitlab.gnome.org/GNOME/gtk/issues/997
156 if (menuPosition.x() + menuRequisition.width > area.x + area.width)
157 menuPosition.setX(area.x + area.width - menuRequisition.width);
158
159 if (menuPosition.y() + information.elementRect.height() + menuRequisition.height <= area.y + area.height
160 || menuPosition.y() - area.y < (area.y + area.height) - (menuPosition.y() + information.elementRect.height()))
161 menuPosition.move(0, information.elementRect.height());
162 else
163 menuPosition.move(0, -menuRequisition.height);
164 gtk_window_move(GTK_WINDOW(m_popup), menuPosition.x(), menuPosition.y());
165
166 auto* toplevel = gtk_widget_get_toplevel(m_webView);
167 if (GTK_IS_WINDOW(toplevel)) {
168 gtk_window_set_transient_for(GTK_WINDOW(m_popup), GTK_WINDOW(toplevel));
169 gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(m_popup));
170 }
171 gtk_window_set_attached_to(GTK_WINDOW(m_popup), m_webView);
172 gtk_window_set_screen(GTK_WINDOW(m_popup), gtk_widget_get_screen(m_webView));
173
174 gtk_widget_show(m_popup);
175}
176
177void WebDataListSuggestionsDropdownGtk::handleKeydownWithIdentifier(const String& key)
178{
179 auto* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(m_treeView));
180 GtkTreeModel* model;
181 GtkTreeIter iter;
182 bool hasSelection = gtk_tree_selection_get_selected(selection, &model, &iter);
183 if (key == "Enter") {
184 if (hasSelection) {
185 GUniqueOutPtr<char> item;
186 gtk_tree_model_get(model, &iter, 0, &item.outPtr(), -1);
187 m_page->didSelectOption(String::fromUTF8(item.get()));
188 }
189 close();
190 return;
191 }
192
193 if (key == "Up") {
194 if ((hasSelection && gtk_tree_model_iter_previous(model, &iter)) || gtk_tree_model_iter_nth_child(model, &iter, nullptr, gtk_tree_model_iter_n_children(model, nullptr) - 1))
195 gtk_tree_selection_select_iter(selection, &iter);
196 else
197 return;
198 } else if (key == "Down") {
199 if ((hasSelection && gtk_tree_model_iter_next(model, &iter)) || gtk_tree_model_get_iter_first(model, &iter))
200 gtk_tree_selection_select_iter(selection, &iter);
201 else
202 return;
203 }
204
205 GUniquePtr<GtkTreePath> path(gtk_tree_model_get_path(model, &iter));
206 gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, FALSE, 0, 0);
207}
208
209void WebDataListSuggestionsDropdownGtk::close()
210{
211 gtk_widget_hide(m_popup);
212}
213
214} // namespace WebKit
215
216#endif // ENABLE(DATALIST_ELEMENT)