blob: 8eb4079291a6e83d198d0caf1ea84f356cbcf746 [file] [log] [blame]
/*
* Copyright (C) 2013, 2020 Igalia S.L.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "cmakeconfig.h"
#include "BrowserSearchBox.h"
struct _BrowserSearchBox {
GtkBox parent;
WebKitWebView *webView;
GtkWidget *entry;
GtkWidget *prevButton;
GtkWidget *nextButton;
GActionGroup *actionGroup;
GMenu *optionsMenu;
GtkWidget *optionsPopover;
GtkWidget *caseCheckButton;
GtkWidget *begginigWordCheckButton;
GtkWidget *capitalAsBegginigWordCheckButton;
};
G_DEFINE_TYPE(BrowserSearchBox, browser_search_box, GTK_TYPE_BOX)
static void setFailedStyleForEntry(BrowserSearchBox *searchBox, gboolean failedSearch)
{
if (failedSearch)
gtk_style_context_add_class(gtk_widget_get_style_context(searchBox->entry), "search-failed");
else
gtk_style_context_remove_class(gtk_widget_get_style_context(searchBox->entry), "search-failed");
}
static void doSearch(BrowserSearchBox *searchBox)
{
GtkEntry *entry = GTK_ENTRY(searchBox->entry);
if (!gtk_entry_get_text_length(entry)) {
setFailedStyleForEntry(searchBox, FALSE);
gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_SECONDARY, NULL);
webkit_find_controller_search_finish(webkit_web_view_get_find_controller(searchBox->webView));
return;
}
if (!gtk_entry_get_icon_name(entry, GTK_ENTRY_ICON_SECONDARY))
gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_SECONDARY, "edit-clear-symbolic");
WebKitFindOptions options = WEBKIT_FIND_OPTIONS_WRAP_AROUND;
GAction *action = g_action_map_lookup_action(G_ACTION_MAP(searchBox->actionGroup), "case-sensitive");
GVariant *state = g_action_get_state(action);
if (!g_variant_get_boolean(state))
options |= WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE;
g_variant_unref(state);
action = g_action_map_lookup_action(G_ACTION_MAP(searchBox->actionGroup), "beginning-word");
state = g_action_get_state(action);
if (g_variant_get_boolean(state))
options |= WEBKIT_FIND_OPTIONS_AT_WORD_STARTS;
g_variant_unref(state);
action = g_action_map_lookup_action(G_ACTION_MAP(searchBox->actionGroup), "capital-as-beginning-word");
state = g_action_get_state(action);
if (g_variant_get_boolean(state))
options |= WEBKIT_FIND_OPTIONS_TREAT_MEDIAL_CAPITAL_AS_WORD_START;
g_variant_unref(state);
#if GTK_CHECK_VERSION(3, 98, 5)
const gchar *text = gtk_editable_get_text(GTK_EDITABLE(entry));
#else
const gchar *text = gtk_entry_get_text(entry);
#endif
webkit_find_controller_search(webkit_web_view_get_find_controller(searchBox->webView), text, options, G_MAXUINT);
}
static void searchNext(BrowserSearchBox *searchBox)
{
webkit_find_controller_search_next(webkit_web_view_get_find_controller(searchBox->webView));
}
static void searchPrevious(BrowserSearchBox *searchBox)
{
webkit_find_controller_search_previous(webkit_web_view_get_find_controller(searchBox->webView));
}
static void searchEntryMenuIconPressedCallback(BrowserSearchBox *searchBox, GtkEntryIconPosition iconPosition, GdkEvent *event)
{
if (iconPosition != GTK_ENTRY_ICON_PRIMARY)
return;
#if GTK_CHECK_VERSION(3, 98, 5)
GtkWidget *popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(searchBox->optionsMenu));
gtk_widget_set_parent(popover, GTK_WIDGET(searchBox));
#else
GtkWidget *popover = gtk_popover_menu_new();
gtk_popover_bind_model(GTK_POPOVER(popover), G_MENU_MODEL(searchBox->optionsMenu), NULL);
gtk_popover_set_relative_to(GTK_POPOVER(popover), searchBox->entry);
#endif
searchBox->optionsPopover = popover;
GdkRectangle rect;
gtk_entry_get_icon_area(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, &rect);
gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
gtk_popover_popup(GTK_POPOVER(popover));
}
static void searchEntryClearIconReleasedCallback(BrowserSearchBox *searchBox, GtkEntryIconPosition iconPosition)
{
if (iconPosition != GTK_ENTRY_ICON_SECONDARY)
return;
#if GTK_CHECK_VERSION(3, 98, 5)
gtk_editable_set_text(GTK_EDITABLE(searchBox->entry), "");
#else
gtk_entry_set_text(GTK_ENTRY(searchBox->entry), "");
#endif
}
static void searchEntryChangedCallback(GtkEntry *entry, BrowserSearchBox *searchBox)
{
doSearch(searchBox);
}
static void searchEntryActivatedCallback(BrowserSearchBox *searchBox)
{
searchNext(searchBox);
}
static void searchPreviousButtonCallback(GSimpleAction *action, GVariant *parameter, gpointer userData)
{
searchPrevious(BROWSER_SEARCH_BOX(userData));
}
static void searchNextButtonCallback(GSimpleAction *action, GVariant *parameter, gpointer userData)
{
searchNext(BROWSER_SEARCH_BOX(userData));
}
static void searchMenuCheckButtonToggledCallback(GSimpleAction *action, GVariant *state, gpointer userData)
{
g_simple_action_set_state(action, state);
doSearch(BROWSER_SEARCH_BOX(userData));
}
static void findControllerFailedToFindTextCallback(BrowserSearchBox *searchBox)
{
setFailedStyleForEntry(searchBox, TRUE);
}
static void findControllerFoundTextCallback(BrowserSearchBox *searchBox)
{
setFailedStyleForEntry(searchBox, FALSE);
}
static const GActionEntry actions[] = {
{ "next", searchNextButtonCallback, NULL, NULL, NULL, { 0 } },
{ "previous", searchPreviousButtonCallback, NULL, NULL, NULL, { 0 } },
{ "case-sensitive", NULL, NULL, "false", searchMenuCheckButtonToggledCallback, { 0 } },
{ "beginning-word", NULL, NULL, "false", searchMenuCheckButtonToggledCallback, { 0 } },
{ "capital-as-beginning-word", NULL, NULL, "false", searchMenuCheckButtonToggledCallback, { 0 } }
};
static void browser_search_box_init(BrowserSearchBox *searchBox)
{
GSimpleActionGroup *actionGroup = g_simple_action_group_new();
searchBox->actionGroup = G_ACTION_GROUP(actionGroup);
g_action_map_add_action_entries(G_ACTION_MAP(actionGroup), actions, G_N_ELEMENTS(actions), searchBox);
gtk_widget_insert_action_group(GTK_WIDGET(searchBox), "find", G_ACTION_GROUP(actionGroup));
gtk_orientable_set_orientation(GTK_ORIENTABLE(searchBox), GTK_ORIENTATION_HORIZONTAL);
#if GTK_CHECK_VERSION(3, 98, 5)
gtk_widget_add_css_class(GTK_WIDGET(searchBox), "linked");
gtk_widget_add_css_class(GTK_WIDGET(searchBox), "raised");
#else
GtkStyleContext *styleContext = gtk_widget_get_style_context(GTK_WIDGET(searchBox));
gtk_style_context_add_class(styleContext, GTK_STYLE_CLASS_LINKED);
gtk_style_context_add_class(styleContext, GTK_STYLE_CLASS_RAISED);
#endif
searchBox->entry = gtk_entry_new();
gtk_widget_set_halign(searchBox->entry, GTK_ALIGN_FILL);
gtk_widget_set_hexpand(searchBox->entry, TRUE);
gtk_entry_set_placeholder_text(GTK_ENTRY(searchBox->entry), "Search");
gtk_entry_set_icon_from_icon_name(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, "edit-find-symbolic");
gtk_entry_set_icon_activatable(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, TRUE);
gtk_entry_set_icon_sensitive(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, TRUE);
gtk_entry_set_icon_tooltip_text(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, "Search options");
#if GTK_CHECK_VERSION(3, 98, 5)
gtk_box_append(GTK_BOX(searchBox), searchBox->entry);
#else
gtk_container_add(GTK_CONTAINER(searchBox), searchBox->entry);
gtk_widget_show(searchBox->entry);
#endif
GtkCssProvider *cssProvider = gtk_css_provider_new();
#if GTK_CHECK_VERSION(3, 98, 5)
gtk_css_provider_load_from_data(cssProvider, ".search-failed { background-color: #ff6666; }", -1);
#else
gtk_css_provider_load_from_data(cssProvider, ".search-failed { background-color: #ff6666; }", -1, NULL);
#endif
gtk_style_context_add_provider(gtk_widget_get_style_context(searchBox->entry), GTK_STYLE_PROVIDER(cssProvider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g_object_unref(cssProvider);
#if GTK_CHECK_VERSION(3, 98, 5)
searchBox->prevButton = gtk_button_new_from_icon_name("go-up-symbolic");
#else
searchBox->prevButton = gtk_button_new_from_icon_name("go-up-symbolic", GTK_ICON_SIZE_MENU);
#endif
GtkButton *button = GTK_BUTTON(searchBox->prevButton);
gtk_actionable_set_action_name(GTK_ACTIONABLE(button), "find.previous");
gtk_widget_set_focus_on_click(searchBox->prevButton, FALSE);
#if GTK_CHECK_VERSION(3, 98, 5)
gtk_box_append(GTK_BOX(searchBox), searchBox->prevButton);
#else
gtk_box_pack_start(GTK_BOX(searchBox), searchBox->prevButton, FALSE, FALSE, 0);
gtk_widget_show(searchBox->prevButton);
#endif
#if GTK_CHECK_VERSION(3, 98, 5)
searchBox->nextButton = gtk_button_new_from_icon_name("go-down-symbolic");
#else
searchBox->nextButton = gtk_button_new_from_icon_name("go-down-symbolic", GTK_ICON_SIZE_MENU);
#endif
button = GTK_BUTTON(searchBox->nextButton);
gtk_actionable_set_action_name(GTK_ACTIONABLE(button), "find.next");
gtk_widget_set_focus_on_click(searchBox->nextButton, FALSE);
#if GTK_CHECK_VERSION(3, 98, 5)
gtk_box_append(GTK_BOX(searchBox), searchBox->nextButton);
#else
gtk_box_pack_start(GTK_BOX(searchBox), searchBox->nextButton, FALSE, FALSE, 0);
gtk_widget_show(searchBox->nextButton);
#endif
searchBox->optionsMenu = g_menu_new();
g_menu_append(searchBox->optionsMenu, "Ca_se sensitive", "find.case-sensitive");
g_menu_append(searchBox->optionsMenu, "Only at the _beginning of words", "find.beginning-word");
g_menu_append(searchBox->optionsMenu, "Capital _always as beginning of word", "find.capital-as-beginning-word");
g_signal_connect_swapped(searchBox->entry, "icon-press", G_CALLBACK(searchEntryMenuIconPressedCallback), searchBox);
g_signal_connect_swapped(searchBox->entry, "icon-release", G_CALLBACK(searchEntryClearIconReleasedCallback), searchBox);
g_signal_connect_after(searchBox->entry, "changed", G_CALLBACK(searchEntryChangedCallback), searchBox);
g_signal_connect_swapped(searchBox->entry, "activate", G_CALLBACK(searchEntryActivatedCallback), searchBox);
}
static void browserSearchBoxFinalize(GObject *gObject)
{
BrowserSearchBox *searchBox = BROWSER_SEARCH_BOX(gObject);
if (searchBox->webView) {
g_object_unref(searchBox->webView);
searchBox->webView = NULL;
}
if (searchBox->optionsMenu) {
g_object_unref(searchBox->optionsMenu);
searchBox->optionsMenu = NULL;
}
g_object_unref(searchBox->actionGroup);
G_OBJECT_CLASS(browser_search_box_parent_class)->finalize(gObject);
}
static void browserSearchBoxDispose(GObject *object)
{
#if GTK_CHECK_VERSION(3, 98, 5)
BrowserSearchBox *searchBox = BROWSER_SEARCH_BOX(object);
g_clear_pointer(&searchBox->optionsPopover, gtk_widget_unparent);
#endif
G_OBJECT_CLASS(browser_search_box_parent_class)->dispose(object);
}
#if GTK_CHECK_VERSION(3, 98, 5)
static void browserSearchBoxSizeAllocate(GtkWidget *widget, int width, int height, int baseline)
{
GTK_WIDGET_CLASS(browser_search_box_parent_class)->size_allocate(widget, width, height, baseline);
BrowserSearchBox *searchBox = BROWSER_SEARCH_BOX(widget);
if (searchBox->optionsPopover)
gtk_popover_present(GTK_POPOVER(searchBox->optionsPopover));
}
#endif
static void browser_search_box_class_init(BrowserSearchBoxClass *klass)
{
GObjectClass *gObjectClass = G_OBJECT_CLASS(klass);
gObjectClass->finalize = browserSearchBoxFinalize;
gObjectClass->dispose = browserSearchBoxDispose;
#if GTK_CHECK_VERSION(3, 98, 5)
GtkWidgetClass *widgetClass = GTK_WIDGET_CLASS(klass);
widgetClass->size_allocate = browserSearchBoxSizeAllocate;
#endif
}
GtkWidget *browser_search_box_new(WebKitWebView *webView)
{
g_return_val_if_fail(WEBKIT_IS_WEB_VIEW(webView), NULL);
GtkWidget *searchBox = GTK_WIDGET(g_object_new(BROWSER_TYPE_SEARCH_BOX, NULL));
BROWSER_SEARCH_BOX(searchBox)->webView = g_object_ref(webView);
WebKitFindController *controller = webkit_web_view_get_find_controller(webView);
g_signal_connect_swapped(controller, "failed-to-find-text", G_CALLBACK(findControllerFailedToFindTextCallback), searchBox);
g_signal_connect_swapped(controller, "found-text", G_CALLBACK(findControllerFoundTextCallback), searchBox);
return searchBox;
}
GtkEntry *browser_search_box_get_entry(BrowserSearchBox *searchBox)
{
g_return_val_if_fail(BROWSER_IS_SEARCH_BOX(searchBox), NULL);
return GTK_ENTRY(searchBox->entry);
}