Changeset 232390 in webkit


Ignore:
Timestamp:
Jun 1, 2018 12:44:31 AM (6 years ago)
Author:
Carlos Garcia Campos
Message:

[GTK] Switch to use a popup window with a tree view instead of a menu for option menu default implementation
https://bugs.webkit.org/show_bug.cgi?id=186146

Reviewed by Michael Catanzaro.

Source/WebCore:

Make it possible to use GUniquePtr with GtkTreePath.

  • platform/gtk/GUniquePtrGtk.h:

Source/WebKit:

It's more convenient to use than the menu.

  • UIProcess/API/gtk/WebKitPopupMenu.cpp:

(WebKit::menuCloseCallback):
(WebKit::WebKitPopupMenu::activateItem):

  • UIProcess/API/gtk/WebKitPopupMenu.h:
  • UIProcess/gtk/WebPopupMenuProxyGtk.cpp:

(WebKit::WebPopupMenuProxyGtk::WebPopupMenuProxyGtk):
(WebKit::WebPopupMenuProxyGtk::selectItem):
(WebKit::WebPopupMenuProxyGtk::activateItem):
(WebKit::WebPopupMenuProxyGtk::activateItemAtPath):
(WebKit::WebPopupMenuProxyGtk::treeViewRowActivatedCallback):
(WebKit::WebPopupMenuProxyGtk::treeViewButtonReleaseEventCallback):
(WebKit::WebPopupMenuProxyGtk::buttonPressEventCallback):
(WebKit::WebPopupMenuProxyGtk::keyPressEventCallback):
(WebKit::WebPopupMenuProxyGtk::createPopupMenu):
(WebKit::WebPopupMenuProxyGtk::show):
(WebKit::WebPopupMenuProxyGtk::showPopupMenu):
(WebKit::WebPopupMenuProxyGtk::hidePopupMenu):
(WebKit::WebPopupMenuProxyGtk::cancelTracking):
(WebKit::WebPopupMenuProxyGtk::typeAheadFindIndex):
(WebKit::WebPopupMenuProxyGtk::typeAheadFind):

  • UIProcess/gtk/WebPopupMenuProxyGtk.h:
Location:
trunk/Source
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebCore/ChangeLog

    r232375 r232390  
     12018-06-01  Carlos Garcia Campos  <cgarcia@igalia.com>
     2
     3        [GTK] Switch to use a popup window with a tree view instead of a menu for option menu default implementation
     4        https://bugs.webkit.org/show_bug.cgi?id=186146
     5
     6        Reviewed by Michael Catanzaro.
     7
     8        Make it possible to use GUniquePtr with GtkTreePath.
     9
     10        * platform/gtk/GUniquePtrGtk.h:
     11
    1122018-05-31  Per Arne Vollan  <pvollan@apple.com>
    213
  • trunk/Source/WebCore/platform/gtk/GUniquePtrGtk.h

    r185502 r232390  
    2828WTF_DEFINE_GPTR_DELETER(GdkEvent, gdk_event_free)
    2929WTF_DEFINE_GPTR_DELETER(GtkIconInfo, gtk_icon_info_free)
     30WTF_DEFINE_GPTR_DELETER(GtkTreePath, gtk_tree_path_free)
    3031
    3132} // namespace WTF
  • trunk/Source/WebKit/ChangeLog

    r232375 r232390  
     12018-06-01  Carlos Garcia Campos  <cgarcia@igalia.com>
     2
     3        [GTK] Switch to use a popup window with a tree view instead of a menu for option menu default implementation
     4        https://bugs.webkit.org/show_bug.cgi?id=186146
     5
     6        Reviewed by Michael Catanzaro.
     7
     8        It's more convenient to use than the menu.
     9
     10        * UIProcess/API/gtk/WebKitPopupMenu.cpp:
     11        (WebKit::menuCloseCallback):
     12        (WebKit::WebKitPopupMenu::activateItem):
     13        * UIProcess/API/gtk/WebKitPopupMenu.h:
     14        * UIProcess/gtk/WebPopupMenuProxyGtk.cpp:
     15        (WebKit::WebPopupMenuProxyGtk::WebPopupMenuProxyGtk):
     16        (WebKit::WebPopupMenuProxyGtk::selectItem):
     17        (WebKit::WebPopupMenuProxyGtk::activateItem):
     18        (WebKit::WebPopupMenuProxyGtk::activateItemAtPath):
     19        (WebKit::WebPopupMenuProxyGtk::treeViewRowActivatedCallback):
     20        (WebKit::WebPopupMenuProxyGtk::treeViewButtonReleaseEventCallback):
     21        (WebKit::WebPopupMenuProxyGtk::buttonPressEventCallback):
     22        (WebKit::WebPopupMenuProxyGtk::keyPressEventCallback):
     23        (WebKit::WebPopupMenuProxyGtk::createPopupMenu):
     24        (WebKit::WebPopupMenuProxyGtk::show):
     25        (WebKit::WebPopupMenuProxyGtk::showPopupMenu):
     26        (WebKit::WebPopupMenuProxyGtk::hidePopupMenu):
     27        (WebKit::WebPopupMenuProxyGtk::cancelTracking):
     28        (WebKit::WebPopupMenuProxyGtk::typeAheadFindIndex):
     29        (WebKit::WebPopupMenuProxyGtk::typeAheadFind):
     30        * UIProcess/gtk/WebPopupMenuProxyGtk.h:
     31
    1322018-05-31  Per Arne Vollan  <pvollan@apple.com>
    233
  • trunk/Source/WebKit/UIProcess/API/gtk/WebKitPopupMenu.cpp

    r218325 r232390  
    3636static void menuCloseCallback(WebKitPopupMenu* popupMenu)
    3737{
    38     popupMenu->activateItem(-1);
     38    popupMenu->activateItem(std::nullopt);
    3939}
    4040
     
    7070}
    7171
    72 void WebKitPopupMenu::selectItem(unsigned itemIndex)
     72void WebKitPopupMenu::activateItem(std::optional<unsigned> itemIndex)
    7373{
    74     if (!m_menu)
    75         return;
    76     m_client->setTextFromItemForPopupMenu(this, itemIndex);
    77     m_selectedItem = itemIndex;
    78 }
    79 
    80 void WebKitPopupMenu::activateItem(int32_t itemIndex)
    81 {
    82     if (!m_menu)
    83         return;
    84     m_client->valueChangedForPopupMenu(this, itemIndex == -1 ? m_selectedItem.value_or(-1) : itemIndex);
    85     g_signal_handlers_disconnect_matched(m_menu.get(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
    86     m_menu = nullptr;
     74    WebPopupMenuProxyGtk::activateItem(itemIndex);
     75    if (m_menu) {
     76        g_signal_handlers_disconnect_matched(m_menu.get(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
     77        m_menu = nullptr;
     78    }
    8779}
    8880
  • trunk/Source/WebKit/UIProcess/API/gtk/WebKitPopupMenu.h

    r218325 r232390  
    3535    ~WebKitPopupMenu() = default;
    3636
    37     void selectItem(unsigned);
    38     void activateItem(int32_t);
     37    void activateItem(std::optional<unsigned> itemIndex) override;
    3938
    4039private:
     
    4645
    4746    GRefPtr<WebKitOptionMenu> m_menu;
    48     std::optional<unsigned> m_selectedItem;
    4947};
    5048
  • trunk/Source/WebKit/UIProcess/gtk/WebPopupMenuProxyGtk.cpp

    r228373 r232390  
    3838using namespace WebCore;
    3939
     40enum Columns {
     41    Label,
     42    Tooltip,
     43    IsGroup,
     44    IsSelected,
     45    IsEnabled,
     46    Index,
     47
     48    Count
     49};
     50
    4051WebPopupMenuProxyGtk::WebPopupMenuProxyGtk(GtkWidget* webView, WebPopupMenuProxy::Client& client)
    4152    : WebPopupMenuProxy(client)
    4253    , m_webView(webView)
    43     , m_dismissMenuTimer(RunLoop::main(), this, &WebPopupMenuProxyGtk::dismissMenuTimerFired)
    4454{
    4555}
     
    5060}
    5161
    52 void WebPopupMenuProxyGtk::populatePopupMenu(const Vector<WebPopupItem>& items)
    53 {
    54     int itemIndex = 0;
     62void WebPopupMenuProxyGtk::selectItem(unsigned itemIndex)
     63{
     64    if (m_client)
     65        m_client->setTextFromItemForPopupMenu(this, itemIndex);
     66    m_selectedItem = itemIndex;
     67}
     68
     69void WebPopupMenuProxyGtk::activateItem(std::optional<unsigned> itemIndex)
     70{
     71    if (m_client)
     72        m_client->valueChangedForPopupMenu(this, itemIndex.value_or(m_selectedItem.value_or(-1)));
     73}
     74
     75bool 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
     91void WebPopupMenuProxyGtk::treeViewRowActivatedCallback(GtkTreeView*, GtkTreePath* path, GtkTreeViewColumn*, WebPopupMenuProxyGtk* popupMenu)
     92{
     93    popupMenu->activateItemAtPath(path);
     94}
     95
     96gboolean 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
     108gboolean 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
     117gboolean 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
     135void 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());
    55143    for (const auto& item : items) {
    56         if (item.m_type == WebPopupItem::Separator) {
    57             GtkWidget* menuItem = gtk_separator_menu_item_new();
    58             gtk_menu_shell_append(GTK_MENU_SHELL(m_popup), menuItem);
    59             gtk_widget_show(menuItem);
     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);
    60152        } else {
    61             GtkWidget* menuItem = gtk_menu_item_new_with_label(item.m_text.utf8().data());
    62             gtk_widget_set_tooltip_text(menuItem, item.m_toolTip.utf8().data());
    63             gtk_widget_set_sensitive(menuItem, item.m_isEnabled);
    64             g_object_set_data(G_OBJECT(menuItem), "popup-menu-item-index", GINT_TO_POINTER(itemIndex));
    65             g_signal_connect(menuItem, "activate", G_CALLBACK(menuItemActivated), this);
    66             g_signal_connect(menuItem, "select", G_CALLBACK(selectItemCallback), this);
    67             gtk_menu_shell_append(GTK_MENU_SHELL(m_popup), menuItem);
    68             gtk_widget_show(menuItem);
     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)));
    69168        }
    70         itemIndex++;
    71     }
     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
     221void 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);
    72231}
    73232
    74233void WebPopupMenuProxyGtk::showPopupMenu(const IntRect& rect, TextDirection, double /* pageScaleFactor */, const Vector<WebPopupItem>& items, const PlatformPopupMenuData&, int32_t selectedIndex)
    75234{
    76     ASSERT(!m_popup);
    77     m_popup = gtk_menu_new();
    78     g_signal_connect(m_popup, "key-press-event", G_CALLBACK(keyPressEventCallback), this);
    79     g_signal_connect(m_popup, "unmap", G_CALLBACK(menuUnmappedCallback), this);
    80 
    81     populatePopupMenu(items);
    82     gtk_menu_set_active(GTK_MENU(m_popup), selectedIndex);
    83 
    84     resetTypeAheadFindState();
    85 
     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    auto* monitor = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(m_webView));
     251    GdkRectangle area;
     252    gdk_monitor_get_workarea(monitor, &area);
     253    int width = std::min(rect.width(), area.width);
     254    size_t itemCount = std::min<size_t>(items.size(), (area.height / 3) / itemHeight);
     255
     256    auto* swindow = GTK_SCROLLED_WINDOW(gtk_bin_get_child(GTK_BIN(m_popup)));
     257    // Disable scrollbars when there's only one item to ensure the scrolled window doesn't take them into account when calculating its minimum size.
     258    gtk_scrolled_window_set_policy(swindow, GTK_POLICY_NEVER, itemCount > 1 ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER);
     259    gtk_widget_realize(m_treeView);
     260    gtk_tree_view_columns_autosize(GTK_TREE_VIEW(m_treeView));
     261    gtk_scrolled_window_set_min_content_width(swindow, width);
     262    gtk_widget_set_size_request(m_popup, width, -1);
     263    gtk_scrolled_window_set_min_content_height(swindow, itemCount * itemHeight);
     264
     265    GtkRequisition menuRequisition;
     266    gtk_widget_get_preferred_size(m_popup, &menuRequisition, nullptr);
    86267    IntPoint menuPosition = convertWidgetPointToScreenPoint(m_webView, rect.location());
    87     menuPosition.move(0, rect.height());
    88 
    89     // This approach follows the one in gtkcombobox.c.
    90     GtkRequisition requisition;
    91     gtk_widget_set_size_request(m_popup, -1, -1);
    92     gtk_widget_get_preferred_size(m_popup, &requisition, nullptr);
    93     gtk_widget_set_size_request(m_popup, std::max(rect.width(), requisition.width), -1);
    94 
    95     if (int itemCount = items.size()) {
    96         GUniquePtr<GList> children(gtk_container_get_children(GTK_CONTAINER(m_popup)));
    97         int i;
    98         GList* child;
    99         for (i = 0, child = children.get(); i < itemCount; i++, child = g_list_next(child)) {
    100             if (i > selectedIndex)
    101                 break;
    102 
    103             GtkWidget* item = GTK_WIDGET(child->data);
    104             GtkRequisition itemRequisition;
    105             gtk_widget_get_preferred_size(item, &itemRequisition, nullptr);
    106             menuPosition.setY(menuPosition.y() - itemRequisition.height);
    107         }
    108     } else {
    109         // Center vertically the empty popup in the combo box area.
    110         menuPosition.setY(menuPosition.y() - rect.height() / 2);
    111     }
    112 
    113     gtk_menu_attach_to_widget(GTK_MENU(m_popup), GTK_WIDGET(m_webView), nullptr);
     268    // FIXME: We can't ensure the menu will be on screen in Wayland.
     269    // https://blog.gtk.org/2016/07/15/future-of-relative-window-positioning/
     270    // https://gitlab.gnome.org/GNOME/gtk/issues/997
     271    if (menuPosition.x() + menuRequisition.width > area.x + area.width)
     272        menuPosition.setX(area.x + area.width - menuRequisition.width);
     273
     274    if (menuPosition.y() + rect.height() + menuRequisition.height <= area.y + area.height
     275        || menuPosition.y() - area.y < (area.y + area.height) - (menuPosition.y() + rect.height()))
     276        menuPosition.move(0, rect.height());
     277    else
     278        menuPosition.move(0, -menuRequisition.height);
     279    gtk_window_move(GTK_WINDOW(m_popup), menuPosition.x(), menuPosition.y());
     280
     281    auto* toplevel = gtk_widget_get_toplevel(m_webView);
     282    if (GTK_IS_WINDOW(toplevel)) {
     283        gtk_window_set_transient_for(GTK_WINDOW(m_popup), GTK_WINDOW(toplevel));
     284        gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(m_popup));
     285    }
     286    gtk_window_set_attached_to(GTK_WINDOW(m_popup), m_webView);
     287    gtk_window_set_screen(GTK_WINDOW(m_popup), gtk_widget_get_screen(m_webView));
    114288
    115289    const GdkEvent* event = m_client->currentlyProcessedMouseDownEvent() ? m_client->currentlyProcessedMouseDownEvent()->nativeEvent() : nullptr;
    116     gtk_menu_popup_for_device(GTK_MENU(m_popup), event ? gdk_event_get_device(event) : nullptr, nullptr, nullptr,
    117         [](GtkMenu*, gint* x, gint* y, gboolean* pushIn, gpointer userData) {
    118             // We can pass a pointer to the menuPosition local variable because the nested main loop ensures this is called in the function context.
    119             IntPoint* menuPosition = static_cast<IntPoint*>(userData);
    120             *x = menuPosition->x();
    121             *y = menuPosition->y();
    122             *pushIn = menuPosition->y() < 0;
    123         }, &menuPosition, nullptr, event && event->type == GDK_BUTTON_PRESS ? event->button.button : 1,
    124         event ? gdk_event_get_time(event) : GDK_CURRENT_TIME);
    125 
    126     // Now that the menu has a position, schedule a resize to make sure it's resized to fit vertically in the work area.
    127     gtk_widget_queue_resize(m_popup);
     290    m_device = event ? gdk_event_get_device(event) : nullptr;
     291    if (!m_device)
     292        m_device = gtk_get_current_event_device();
     293    if (m_device && gdk_device_get_display(m_device) != display)
     294        m_device = nullptr;
     295    if (!m_device)
     296        m_device = gdk_seat_get_pointer(gdk_display_get_default_seat(display));
     297    ASSERT(m_device);
     298    if (gdk_device_get_source(m_device) == GDK_SOURCE_KEYBOARD)
     299        m_device = gdk_device_get_associated_device(m_device);
     300
     301#if GTK_CHECK_VERSION(3, 20, 0)
     302    gtk_grab_add(m_popup);
     303    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) {
     304        static_cast<WebPopupMenuProxyGtk*>(userData)->show();
     305    }, this);
     306#else
     307    gtk_device_grab_add(m_popup, m_device, TRUE);
     308    auto grabResult = gdk_device_grab(m_device, gtk_widget_get_window(m_popup), GDK_OWNERSHIP_WINDOW, TRUE,
     309        static_cast<GdkEventMask>(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK), nullptr, GDK_CURRENT_TIME);
     310    show();
     311#endif
    128312
    129313    // PopupMenu can fail to open when there is no mouse grab.
    130314    // Ensure WebCore does not go into some pesky state.
    131     if (!gtk_widget_get_visible(m_popup)) {
     315    if (grabResult != GDK_GRAB_SUCCESS) {
    132316       m_client->failedToShowPopupMenu();
    133317       return;
    134318    }
    135 
    136     // This ensures that the active item gets selected after popping up the menu, and
    137     // as it says in "gtkcombobox.c" (line ~1606): it's ugly, but gets the job done.
    138     GtkWidget* activeChild = gtk_menu_get_active(GTK_MENU(m_popup));
    139     if (activeChild && gtk_widget_get_visible(activeChild))
    140         gtk_menu_shell_select_item(GTK_MENU_SHELL(m_popup), activeChild);
    141319}
    142320
     
    146324        return;
    147325
    148     gtk_menu_popdown(GTK_MENU(m_popup));
    149     resetTypeAheadFindState();
     326    if (m_device) {
     327#if GTK_CHECK_VERSION(3, 20, 0)
     328        gdk_seat_ungrab(gdk_device_get_seat(m_device));
     329        gtk_grab_remove(m_popup);
     330#else
     331        gdk_device_ungrab(m_device, GDK_CURRENT_TIME);
     332        gtk_device_grab_remove(m_popup, m_device);
     333#endif
     334        gtk_window_set_transient_for(GTK_WINDOW(m_popup), nullptr);
     335        gtk_window_set_attached_to(GTK_WINDOW(m_popup), nullptr);
     336        m_device = nullptr;
     337    }
     338
     339    activateItem(std::nullopt);
     340
     341    if (m_currentSearchString) {
     342        g_string_free(m_currentSearchString, TRUE);
     343        m_currentSearchString = nullptr;
     344    }
     345
     346    gtk_widget_destroy(m_popup);
     347    m_popup = nullptr;
    150348}
    151349
     
    155353        return;
    156354
    157     m_dismissMenuTimer.stop();
    158355    g_signal_handlers_disconnect_matched(m_popup, G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this);
    159356    hidePopupMenu();
    160     gtk_widget_destroy(m_popup);
    161     m_popup = nullptr;
     357}
     358
     359std::optional<unsigned> WebPopupMenuProxyGtk::typeAheadFindIndex(GdkEventKey* event)
     360{
     361    gunichar keychar = gdk_keyval_to_unicode(event->keyval);
     362    if (!g_unichar_isprint(keychar))
     363        return std::nullopt;
     364
     365    if (event->time < m_previousKeyEventTime)
     366        return std::nullopt;
     367
     368    static const uint32_t typeaheadTimeoutMs = 1000;
     369    if (event->time - m_previousKeyEventTime > typeaheadTimeoutMs) {
     370        if (m_currentSearchString)
     371            g_string_truncate(m_currentSearchString, 0);
     372    }
     373    m_previousKeyEventTime = event->time;
     374
     375    if (!m_currentSearchString)
     376        m_currentSearchString = g_string_new(nullptr);
     377    g_string_append_unichar(m_currentSearchString, keychar);
     378
     379    int prefixLength = -1;
     380    if (keychar == m_repeatingCharacter)
     381        prefixLength = 1;
     382    else
     383        m_repeatingCharacter = m_currentSearchString->len == 1 ? keychar : '\0';
     384
     385    GtkTreeModel* model;
     386    GtkTreeIter iter;
     387    guint selectedIndex = 0;
     388    if (gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(m_treeView)), &model, &iter))
     389        gtk_tree_model_get(model, &iter, Columns::Index, &selectedIndex, -1);
     390
     391    unsigned index = selectedIndex;
     392    if (m_repeatingCharacter != '\0')
     393        index++;
     394    auto itemCount = m_paths.size();
     395    index %= itemCount;
     396
     397    GUniquePtr<char> normalizedPrefix(g_utf8_normalize(m_currentSearchString->str, prefixLength, G_NORMALIZE_ALL));
     398    GUniquePtr<char> prefix(normalizedPrefix ? g_utf8_casefold(normalizedPrefix.get(), -1) : nullptr);
     399    if (!prefix)
     400        return std::nullopt;
     401
     402    model = gtk_tree_view_get_model(GTK_TREE_VIEW(m_treeView));
     403    for (unsigned i = 0; i < itemCount; i++, index = (index + 1) % itemCount) {
     404        auto& path = m_paths[index];
     405        if (!path || !gtk_tree_model_get_iter(model, &iter, path.get()))
     406            continue;
     407
     408        GUniqueOutPtr<char> label;
     409        gboolean isEnabled;
     410        gtk_tree_model_get(model, &iter, Columns::Label, &label.outPtr(), Columns::IsEnabled, &isEnabled, -1);
     411        if (!isEnabled)
     412            continue;
     413
     414        GUniquePtr<char> normalizedText(g_utf8_normalize(label.get(), -1, G_NORMALIZE_ALL));
     415        GUniquePtr<char> text(normalizedText ? g_utf8_casefold(normalizedText.get(), -1) : nullptr);
     416        if (!text)
     417            continue;
     418
     419        if (!strncmp(prefix.get(), text.get(), strlen(prefix.get())))
     420            return index;
     421    }
     422
     423    return std::nullopt;
    162424}
    163425
    164426bool WebPopupMenuProxyGtk::typeAheadFind(GdkEventKey* event)
    165427{
    166     // If we were given a non-printable character just skip it.
    167     gunichar unicodeCharacter = gdk_keyval_to_unicode(event->keyval);
    168     if (!g_unichar_isprint(unicodeCharacter)) {
    169         resetTypeAheadFindState();
     428    auto searchIndex = typeAheadFindIndex(event);
     429    if (!searchIndex)
    170430        return false;
    171     }
    172 
    173     glong charactersWritten;
    174     GUniquePtr<gunichar2> utf16String(g_ucs4_to_utf16(&unicodeCharacter, 1, nullptr, &charactersWritten, nullptr));
    175     if (!utf16String) {
    176         resetTypeAheadFindState();
    177         return false;
    178     }
    179 
    180     // If the character is the same as the last character, the user is probably trying to
    181     // cycle through the menulist entries. This matches the WebCore behavior for collapsed menulists.
    182     static const uint32_t searchTimeoutMs = 1000;
    183     bool repeatingCharacter = unicodeCharacter != m_previousKeyEventCharacter;
    184     if (event->time - m_previousKeyEventTimestamp > searchTimeoutMs)
    185         m_currentSearchString = String(reinterpret_cast<UChar*>(utf16String.get()), charactersWritten);
    186     else if (repeatingCharacter)
    187         m_currentSearchString.append(String(reinterpret_cast<UChar*>(utf16String.get()), charactersWritten));
    188 
    189     m_previousKeyEventTimestamp = event->time;
    190     m_previousKeyEventCharacter = unicodeCharacter;
    191 
    192     GUniquePtr<GList> children(gtk_container_get_children(GTK_CONTAINER(m_popup)));
    193     if (!children)
    194         return true;
    195 
    196     // We case fold before searching, because strncmp does not handle non-ASCII characters.
    197     GUniquePtr<gchar> searchStringWithCaseFolded(g_utf8_casefold(m_currentSearchString.utf8().data(), -1));
    198     size_t prefixLength = strlen(searchStringWithCaseFolded.get());
    199 
    200     // If a menu item has already been selected, start searching from the current
    201     // item down the list. This will make multiple key presses of the same character
    202     // advance the selection.
    203     GList* currentChild = children.get();
    204     if (m_currentlySelectedMenuItem) {
    205         currentChild = g_list_find(children.get(), m_currentlySelectedMenuItem);
    206         if (!currentChild) {
    207             m_currentlySelectedMenuItem = nullptr;
    208             currentChild = children.get();
    209         }
    210 
    211         // Repeating characters should iterate.
    212         if (repeatingCharacter) {
    213             if (GList* nextChild = g_list_next(currentChild))
    214                 currentChild = nextChild;
    215         }
    216     }
    217 
    218     GList* firstChild = currentChild;
    219     do {
    220         currentChild = g_list_next(currentChild);
    221         if (!currentChild)
    222             currentChild = children.get();
    223 
    224         GUniquePtr<gchar> itemText(g_utf8_casefold(gtk_menu_item_get_label(GTK_MENU_ITEM(currentChild->data)), -1));
    225         if (!strncmp(searchStringWithCaseFolded.get(), itemText.get(), prefixLength)) {
    226             gtk_menu_shell_select_item(GTK_MENU_SHELL(m_popup), GTK_WIDGET(currentChild->data));
    227             break;
    228         }
    229     } while (currentChild != firstChild);
     431
     432    auto& path = m_paths[searchIndex.value()];
     433    ASSERT(path);
     434    gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, TRUE, 0.5, 0);
     435    gtk_tree_view_set_cursor(GTK_TREE_VIEW(m_treeView), path.get(), nullptr, FALSE);
     436    selectItem(searchIndex.value());
    230437
    231438    return true;
    232439}
    233440
    234 void WebPopupMenuProxyGtk::resetTypeAheadFindState()
    235 {
    236     m_currentlySelectedMenuItem = nullptr;
    237     m_previousKeyEventCharacter = 0;
    238     m_previousKeyEventTimestamp = 0;
    239     m_currentSearchString = emptyString();
    240 }
    241 
    242 void WebPopupMenuProxyGtk::menuItemActivated(GtkMenuItem* menuItem, WebPopupMenuProxyGtk* popupMenu)
    243 {
    244     popupMenu->m_dismissMenuTimer.stop();
    245     if (popupMenu->m_client)
    246         popupMenu->m_client->valueChangedForPopupMenu(popupMenu, GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuItem), "popup-menu-item-index")));
    247 }
    248 
    249 void WebPopupMenuProxyGtk::dismissMenuTimerFired()
    250 {
    251     if (m_client)
    252         m_client->valueChangedForPopupMenu(this, -1);
    253 }
    254 
    255 void WebPopupMenuProxyGtk::menuUnmappedCallback(GtkWidget*, WebPopupMenuProxyGtk* popupMenu)
    256 {
    257     if (!popupMenu->m_client)
    258         return;
    259 
    260     // When an item is activated, the menu is first hidden and then activate signal is emitted, so at this point we don't know
    261     // if the menu has been hidden because an item has been selected or because the menu has been dismissed. Wait until the next
    262     // main loop iteration to dismiss the menu, if an item is activated the timer will be cancelled.
    263     popupMenu->m_dismissMenuTimer.startOneShot(0_s);
    264 }
    265 
    266 void WebPopupMenuProxyGtk::selectItemCallback(GtkWidget* item, WebPopupMenuProxyGtk* popupMenu)
    267 {
    268     popupMenu->setCurrentlySelectedMenuItem(item);
    269 }
    270 
    271 gboolean WebPopupMenuProxyGtk::keyPressEventCallback(GtkWidget*, GdkEventKey* event, WebPopupMenuProxyGtk* popupMenu)
    272 {
    273     return popupMenu->typeAheadFind(event);
    274 }
    275 
    276441} // namespace WebKit
  • trunk/Source/WebKit/UIProcess/gtk/WebPopupMenuProxyGtk.h

    r218329 r232390  
    1818 */
    1919
    20 #ifndef WebPopupMenuProxyGtk_h
    21 #define WebPopupMenuProxyGtk_h
     20#pragma once
    2221
    2322#include "WebPopupMenuProxy.h"
    24 #include <wtf/RunLoop.h>
     23#include <WebCore/GUniquePtrGtk.h>
     24#include <wtf/Vector.h>
    2525#include <wtf/glib/GRefPtr.h>
    2626#include <wtf/text/WTFString.h>
    2727
    2828typedef struct _GMainLoop GMainLoop;
     29typedef struct _GdkDevice GdkDevice;
     30typedef struct _GdkEventButton GdkEventButton;
    2931typedef struct _GdkEventKey GdkEventKey;
     32typedef struct _GtkTreePath GtkTreePath;
     33typedef struct _GtkTreeView GtkTreeView;
     34typedef struct _GtkTreeViewColumn GtkTreeViewColumn;
    3035
    3136namespace WebCore {
     
    4954    void cancelTracking() override;
    5055
     56    virtual void selectItem(unsigned itemIndex);
     57    virtual void activateItem(std::optional<unsigned> itemIndex);
     58
    5159protected:
    5260    WebPopupMenuProxyGtk(GtkWidget*, WebPopupMenuProxy::Client&);
     
    5563
    5664private:
    57     void setCurrentlySelectedMenuItem(GtkWidget* item) { m_currentlySelectedMenuItem = item; }
    58     void populatePopupMenu(const Vector<WebPopupItem>&);
    59     void dismissMenuTimerFired();
     65    void createPopupMenu(const Vector<WebPopupItem>&, int32_t selectedIndex);
     66    void show();
     67    bool activateItemAtPath(GtkTreePath*);
     68    std::optional<unsigned> typeAheadFindIndex(GdkEventKey*);
     69    bool typeAheadFind(GdkEventKey*);
    6070
    61     bool typeAheadFind(GdkEventKey*);
    62     void resetTypeAheadFindState();
    63 
    64     static void menuItemActivated(GtkMenuItem*, WebPopupMenuProxyGtk*);
    65     static void selectItemCallback(GtkWidget*, WebPopupMenuProxyGtk*);
     71    static gboolean buttonPressEventCallback(GtkWidget*, GdkEventButton*, WebPopupMenuProxyGtk*);
    6672    static gboolean keyPressEventCallback(GtkWidget*, GdkEventKey*, WebPopupMenuProxyGtk*);
    67     static void menuUnmappedCallback(GtkWidget*, WebPopupMenuProxyGtk*);
     73    static void treeViewRowActivatedCallback(GtkTreeView*, GtkTreePath*, GtkTreeViewColumn*, WebPopupMenuProxyGtk*);
     74    static gboolean treeViewButtonReleaseEventCallback(GtkWidget*, GdkEventButton*, WebPopupMenuProxyGtk*);
    6875
    6976    GtkWidget* m_popup { nullptr };
     77    GtkWidget* m_treeView { nullptr };
     78    GdkDevice* m_device { nullptr };
    7079
    71     RunLoop::Timer<WebPopupMenuProxyGtk> m_dismissMenuTimer;
     80    Vector<GUniquePtr<GtkTreePath>> m_paths;
     81    std::optional<unsigned> m_selectedItem;
    7282
    7383    // Typeahead find.
    74     unsigned m_previousKeyEventCharacter { 0 };
    75     uint32_t m_previousKeyEventTimestamp { 0 };
    76     GtkWidget* m_currentlySelectedMenuItem { nullptr };
    77     String m_currentSearchString;
     84    gunichar m_repeatingCharacter { '\0' };
     85    uint32_t m_previousKeyEventTime { 0 };
     86    GString* m_currentSearchString { nullptr };
    7887};
    7988
    8089} // namespace WebKit
    81 
    82 
    83 #endif // WebPopupMenuProxyGtk_h
Note: See TracChangeset for help on using the changeset viewer.