Blob Blame History Raw
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
 * Copyright (C) 2018 Sébastien Wilmet <swilmet@gnome.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#include "dh-web-view.h"
#include <math.h>
#include <glib/gi18n.h>

struct _DhWebViewPrivate {
        gchar *search_text;
        gdouble total_scroll_delta_y;
};

static const gdouble zoom_levels[] = {
        0.5,            /* 50% */
        0.8408964152,   /* 75% */
        1.0,            /* 100% */
        1.1892071149,   /* 125% */
        1.4142135623,   /* 150% */
        1.6817928304,   /* 175% */
        2.0,            /* 200% */
        2.8284271247,   /* 300% */
        4.0             /* 400% */
};

static const guint n_zoom_levels = G_N_ELEMENTS (zoom_levels);

#define ZOOM_DEFAULT (zoom_levels[2])

G_DEFINE_TYPE_WITH_PRIVATE (DhWebView, dh_web_view, WEBKIT_TYPE_WEB_VIEW)

static gint
get_current_zoom_level_index (DhWebView *view)
{
        gdouble zoom_level;
        gdouble previous;
        guint i;

        zoom_level = webkit_web_view_get_zoom_level (WEBKIT_WEB_VIEW (view));

        previous = zoom_levels[0];
        for (i = 1; i < n_zoom_levels; i++) {
                gdouble current = zoom_levels[i];
                gdouble mean = sqrt (previous * current);

                if (zoom_level <= mean)
                        return i - 1;

                previous = current;
        }

        return n_zoom_levels - 1;
}

static void
bump_zoom_level (DhWebView *view,
                 gint       bump_amount)
{
        gint zoom_level_index;
        gdouble new_zoom_level;

        if (bump_amount == 0)
                return;

        zoom_level_index = get_current_zoom_level_index (view) + bump_amount;
        zoom_level_index = CLAMP (zoom_level_index, 0, (gint)n_zoom_levels - 1);
        new_zoom_level = zoom_levels[zoom_level_index];

        webkit_web_view_set_zoom_level (WEBKIT_WEB_VIEW (view), new_zoom_level);
}

static gboolean
dh_web_view_scroll_event (GtkWidget      *widget,
                          GdkEventScroll *scroll_event)
{
        DhWebView *view = DH_WEB_VIEW (widget);
        gdouble delta_y;

        if ((scroll_event->state & GDK_CONTROL_MASK) == 0)
                goto chain_up;

        switch (scroll_event->direction) {
                case GDK_SCROLL_UP:
                        bump_zoom_level (view, 1);
                        return GDK_EVENT_STOP;

                case GDK_SCROLL_DOWN:
                        bump_zoom_level (view, -1);
                        return GDK_EVENT_STOP;

                case GDK_SCROLL_LEFT:
                case GDK_SCROLL_RIGHT:
                        break;

                case GDK_SCROLL_SMOOTH:
                        gdk_event_get_scroll_deltas ((GdkEvent *)scroll_event, NULL, &delta_y);
                        view->priv->total_scroll_delta_y += delta_y;

                        /* Avoiding direct float comparison.
                         * -1 and 1 are the thresholds for bumping the zoom level,
                         * which can be adjusted for taste.
                         */
                        if ((gint)view->priv->total_scroll_delta_y <= -1) {
                                view->priv->total_scroll_delta_y = 0.f;
                                bump_zoom_level (view, 1);
                        } else if ((gint)view->priv->total_scroll_delta_y >= 1) {
                                view->priv->total_scroll_delta_y = 0.f;
                                bump_zoom_level (view, -1);
                        }
                        return GDK_EVENT_STOP;

                default:
                        g_warn_if_reached ();
        }

chain_up:
        if (GTK_WIDGET_CLASS (dh_web_view_parent_class)->scroll_event == NULL)
                return GDK_EVENT_PROPAGATE;

        return GTK_WIDGET_CLASS (dh_web_view_parent_class)->scroll_event (widget, scroll_event);
}

static void
dh_web_view_constructed (GObject *object)
{
        WebKitWebView *view = WEBKIT_WEB_VIEW (object);
        WebKitSettings *settings;

        if (G_OBJECT_CLASS (dh_web_view_parent_class)->constructed != NULL)
                G_OBJECT_CLASS (dh_web_view_parent_class)->constructed (object);

        /* Disable some things we have no need for. */
        settings = webkit_web_view_get_settings (view);
        webkit_settings_set_enable_html5_database (settings, FALSE);
        webkit_settings_set_enable_html5_local_storage (settings, FALSE);
        webkit_settings_set_enable_plugins (settings, FALSE);
}

static void
dh_web_view_finalize (GObject *object)
{
        DhWebView *view = DH_WEB_VIEW (object);

        g_free (view->priv->search_text);

        G_OBJECT_CLASS (dh_web_view_parent_class)->finalize (object);
}

static void
dh_web_view_class_init (DhWebViewClass *klass)
{
        GObjectClass *object_class = G_OBJECT_CLASS (klass);
        GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

        object_class->constructed = dh_web_view_constructed;
        object_class->finalize = dh_web_view_finalize;

        widget_class->scroll_event = dh_web_view_scroll_event;
}

static void
dh_web_view_init (DhWebView *view)
{
        view->priv = dh_web_view_get_instance_private (view);
        view->priv->total_scroll_delta_y = 0.f;

        gtk_widget_set_hexpand (GTK_WIDGET (view), TRUE);
        gtk_widget_set_vexpand (GTK_WIDGET (view), TRUE);
}

DhWebView *
dh_web_view_new (void)
{
        return g_object_new (DH_TYPE_WEB_VIEW, NULL);
}

const gchar *
dh_web_view_get_devhelp_title (DhWebView *view)
{
        const gchar *title;

        g_return_val_if_fail (DH_IS_WEB_VIEW (view), NULL);

        title = webkit_web_view_get_title (WEBKIT_WEB_VIEW (view));

        if (title == NULL || title[0] == '\0')
                title = _("Empty Page");

        return title;
}

/*
 * dh_web_view_set_search_text:
 * @view: a #DhWebView.
 * @search_text: (nullable): the search string, or %NULL.
 *
 * A more convenient API (for Devhelp needs) than #WebKitFindController. If
 * @search_text is not empty, it calls webkit_find_controller_search() if not
 * already done. If @search_text is empty or %NULL, it calls
 * webkit_find_controller_search_finish().
 */
void
dh_web_view_set_search_text (DhWebView   *view,
                             const gchar *search_text)
{
        WebKitFindController *find_controller;

        g_return_if_fail (DH_IS_WEB_VIEW (view));

        if (g_strcmp0 (view->priv->search_text, search_text) == 0)
                return;

        g_free (view->priv->search_text);
        view->priv->search_text = g_strdup (search_text);

        find_controller = webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (view));

        if (search_text != NULL && search_text[0] != '\0') {
                /* If webkit_find_controller_search() is called a second time
                 * with the same parameters it's not a NOP, it launches a new
                 * search, apparently, which screws up search_next() and
                 * search_previous(). So we must call it only once for the same
                 * search string.
                 */
                webkit_find_controller_search (find_controller,
                                               search_text,
                                               WEBKIT_FIND_OPTIONS_WRAP_AROUND |
                                               WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE,
                                               G_MAXUINT);
        } else {
                /* It's fine to call it several times. But unfortunately it
                 * doesn't change the WebKitFindController:text property. So we
                 * must store our own search_text.
                 */
                webkit_find_controller_search_finish (find_controller);
        }
}

void
dh_web_view_search_next (DhWebView *view)
{
        WebKitFindController *find_controller;

        g_return_if_fail (DH_IS_WEB_VIEW (view));

        if (view->priv->search_text == NULL || view->priv->search_text[0] == '\0')
                return;

        find_controller = webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (view));
        webkit_find_controller_search_next (find_controller);
}

void
dh_web_view_search_previous (DhWebView *view)
{
        WebKitFindController *find_controller;

        g_return_if_fail (DH_IS_WEB_VIEW (view));

        if (view->priv->search_text == NULL || view->priv->search_text[0] == '\0')
                return;

        find_controller = webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (view));
        webkit_find_controller_search_previous (find_controller);
}

gboolean
dh_web_view_can_zoom_in (DhWebView *view)
{
        gint zoom_level_index;

        g_return_val_if_fail (DH_IS_WEB_VIEW (view), FALSE);

        zoom_level_index = get_current_zoom_level_index (view);
        return zoom_level_index < ((gint)n_zoom_levels - 1);
}

gboolean
dh_web_view_can_zoom_out (DhWebView *view)
{
        gint zoom_level_index;

        g_return_val_if_fail (DH_IS_WEB_VIEW (view), FALSE);

        zoom_level_index = get_current_zoom_level_index (view);
        return zoom_level_index > 0;
}

gboolean
dh_web_view_can_reset_zoom (DhWebView *view)
{
        gint zoom_level_index;

        g_return_val_if_fail (DH_IS_WEB_VIEW (view), FALSE);

        zoom_level_index = get_current_zoom_level_index (view);
        return zoom_levels[zoom_level_index] != ZOOM_DEFAULT;
}

void
dh_web_view_zoom_in (DhWebView *view)
{
        g_return_if_fail (DH_IS_WEB_VIEW (view));

        bump_zoom_level (view, 1);
}

void
dh_web_view_zoom_out (DhWebView *view)
{
        g_return_if_fail (DH_IS_WEB_VIEW (view));

        bump_zoom_level (view, -1);
}

void
dh_web_view_reset_zoom (DhWebView *view)
{
        g_return_if_fail (DH_IS_WEB_VIEW (view));

        webkit_web_view_set_zoom_level (WEBKIT_WEB_VIEW (view), ZOOM_DEFAULT);
}