WebKitFindController.cpp   [plain text]


/*
 * Copyright (C) 2012 Igalia S.L.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2,1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include "WebKitFindController.h"

#include "WebKitEnumTypes.h"
#include "WebKitPrivate.h"
#include "WebKitWebViewBasePrivate.h"
#include <glib/gi18n-lib.h>
#include <wtf/gobject/GRefPtr.h>
#include <wtf/text/CString.h>

using namespace WebCore;

enum {
    FOUND_TEXT,
    FAILED_TO_FIND_TEXT,
    COUNTED_MATCHES,

    LAST_SIGNAL
};

enum {
    PROP_0,

    PROP_TEXT,
    PROP_OPTIONS,
    PROP_MAX_MATCH_COUNT,
    PROP_WEB_VIEW
};

typedef enum {
    FindOperation,
    CountOperation
} WebKitFindControllerOperation;

struct _WebKitFindControllerPrivate {
    CString searchText;
    uint32_t findOptions;
    unsigned maxMatchCount;
    WebKitWebView* webView;
};

static guint signals[LAST_SIGNAL] = { 0, };

G_DEFINE_TYPE(WebKitFindController, webkit_find_controller, G_TYPE_OBJECT)

COMPILE_ASSERT_MATCHING_ENUM(WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE, kWKFindOptionsCaseInsensitive);
COMPILE_ASSERT_MATCHING_ENUM(WEBKIT_FIND_OPTIONS_AT_WORD_STARTS, kWKFindOptionsAtWordStarts);
COMPILE_ASSERT_MATCHING_ENUM(WEBKIT_FIND_OPTIONS_TREAT_MEDIAL_CAPITAL_AS_WORD_START, kWKFindOptionsTreatMedialCapitalAsWordStart);
COMPILE_ASSERT_MATCHING_ENUM(WEBKIT_FIND_OPTIONS_BACKWARDS, kWKFindOptionsBackwards);
COMPILE_ASSERT_MATCHING_ENUM(WEBKIT_FIND_OPTIONS_WRAP_AROUND, kWKFindOptionsWrapAround);

static void didFindString(WKPageRef page, WKStringRef string, unsigned matchCount, const void* clientInfo)
{
    g_signal_emit(WEBKIT_FIND_CONTROLLER(clientInfo), signals[FOUND_TEXT], 0, matchCount);
}

static void didFailToFindString(WKPageRef page, WKStringRef string, const void* clientInfo)
{
    g_signal_emit(WEBKIT_FIND_CONTROLLER(clientInfo), signals[FAILED_TO_FIND_TEXT], 0);
}

static void didCountStringMatches(WKPageRef page, WKStringRef string, unsigned matchCount, const void* clientInfo)
{
    g_signal_emit(WEBKIT_FIND_CONTROLLER(clientInfo), signals[COUNTED_MATCHES], 0, matchCount);
}

static void webkit_find_controller_init(WebKitFindController* findController)
{
    WebKitFindControllerPrivate* priv = G_TYPE_INSTANCE_GET_PRIVATE(findController, WEBKIT_TYPE_FIND_CONTROLLER, WebKitFindControllerPrivate);
    findController->priv = priv;
    new (priv) WebKitFindControllerPrivate();
}

static WKPageRef inline getWKPageFromWebKitWebView(WebKitWebView* webView)
{
    return toAPI(webkitWebViewBaseGetPage(WEBKIT_WEB_VIEW_BASE(webView)));
}

static void webkitFindControllerConstructed(GObject* object)
{
    WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);
    WKPageFindClient wkFindClient = {
        kWKPageFindClientCurrentVersion,
        findController, // clientInfo
        didFindString,
        didFailToFindString,
        didCountStringMatches
    };

    WKPageSetPageFindClient(getWKPageFromWebKitWebView(findController->priv->webView), &wkFindClient);
}

static void webkitFindControllerGetProperty(GObject* object, guint propId, GValue* value, GParamSpec* paramSpec)
{
    WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);

    switch (propId) {
    case PROP_TEXT:
        g_value_set_string(value, webkit_find_controller_get_search_text(findController));
        break;
    case PROP_OPTIONS:
        g_value_set_uint(value, webkit_find_controller_get_options(findController));
        break;
    case PROP_MAX_MATCH_COUNT:
        g_value_set_uint(value, webkit_find_controller_get_max_match_count(findController));
        break;
    case PROP_WEB_VIEW:
        g_value_set_object(value, webkit_find_controller_get_web_view(findController));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
    }
}

static void webkitFindControllerSetProperty(GObject* object, guint propId, const GValue* value, GParamSpec* paramSpec)
{
    WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);

    switch (propId) {
    case PROP_WEB_VIEW:
        findController->priv->webView = WEBKIT_WEB_VIEW(g_value_get_object(value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
    }
}

static void webkitFindControllerFinalize(GObject* object)
{
    WEBKIT_FIND_CONTROLLER(object)->priv->~WebKitFindControllerPrivate();
    G_OBJECT_CLASS(webkit_find_controller_parent_class)->finalize(object);
}

static void webkit_find_controller_class_init(WebKitFindControllerClass* findClass)
{
    GObjectClass* gObjectClass = G_OBJECT_CLASS(findClass);

    gObjectClass->constructed = webkitFindControllerConstructed;
    gObjectClass->get_property = webkitFindControllerGetProperty;
    gObjectClass->set_property = webkitFindControllerSetProperty;
    gObjectClass->finalize = webkitFindControllerFinalize;

    g_type_class_add_private(findClass, sizeof(WebKitFindControllerPrivate));

    /**
     * WebKitFindController:text:
     *
     * The current search text for this #WebKitFindController.
     */
    g_object_class_install_property(gObjectClass,
                                    PROP_TEXT,
                                    g_param_spec_string("text",
                                                        _("Search text"),
                                                        _("Text to search for in the view"),
                                                        0,
                                                        WEBKIT_PARAM_READABLE));

    /**
     * WebKitFindController:options:
     *
     * The options to be used in the search operation.
     */
    g_object_class_install_property(gObjectClass,
                                    PROP_OPTIONS,
                                    g_param_spec_flags("options",
                                                       _("Search Options"),
                                                       _("Search options to be used in the search operation"),
                                                       WEBKIT_TYPE_FIND_OPTIONS,
                                                       WEBKIT_FIND_OPTIONS_NONE,
                                                       WEBKIT_PARAM_READABLE));

    /**
     * WebKitFindController:max-match-count:
     *
     * The maximum number of matches to report for a given search.
     */
    g_object_class_install_property(gObjectClass,
                                    PROP_MAX_MATCH_COUNT,
                                    g_param_spec_uint("max-match-count",
                                                      _("Maximum matches count"),
                                                      _("The maximum number of matches in a given text to report"),
                                                      0, G_MAXUINT, 0,
                                                      WEBKIT_PARAM_READABLE));

    /**
     * WebKitFindController:web-view:
     *
     * The #WebKitWebView this controller is associated to.
     */
    g_object_class_install_property(gObjectClass,
                                    PROP_WEB_VIEW,
                                    g_param_spec_object("web-view",
                                                        _("WebView"),
                                                        _("The WebView associated with this find controller"),
                                                        WEBKIT_TYPE_WEB_VIEW,
                                                        static_cast<GParamFlags>(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));

    /**
     * WebKitFindController::found-text:
     * @find_controller: the #WebKitFindController
     * @match_count: the number of matches found of the search text
     *
     * This signal is emitted when a given text is found in the web
     * page text. It will be issued if the text is found
     * asynchronously after a call to webkit_find_controller_search(),
     * webkit_find_controller_search_next() or
     * webkit_find_controller_search_previous().
     */
    signals[FOUND_TEXT] =
        g_signal_new("found-text",
                     G_TYPE_FROM_CLASS(gObjectClass),
                     G_SIGNAL_RUN_LAST,
                     0, 0, 0,
                     g_cclosure_marshal_VOID__UINT,
                     G_TYPE_NONE, 1, G_TYPE_UINT);

    /**
     * WebKitFindController::failed-to-find-text:
     * @find_controller: the #WebKitFindController
     *
     * This signal is emitted when a search operation does not find
     * any result for the given text. It will be issued if the text
     * is not found asynchronously after a call to
     * webkit_find_controller_search(), webkit_find_controller_search_next()
     * or webkit_find_controller_search_previous().
     */
    signals[FAILED_TO_FIND_TEXT] =
        g_signal_new("failed-to-find-text",
                     G_TYPE_FROM_CLASS(gObjectClass),
                     G_SIGNAL_RUN_LAST,
                     0, 0, 0,
                     g_cclosure_marshal_VOID__VOID,
                     G_TYPE_NONE, 0);

    /**
     * WebKitFindController::counted-matches:
     * @find_controller: the #WebKitFindController
     * @match_count: the number of matches of the search text
     *
     * This signal is emitted when the #WebKitFindController has
     * counted the number of matches for a given text after a call
     * to webkit_find_controller_count_matches().
     */
    signals[COUNTED_MATCHES] =
        g_signal_new("counted-matches",
                     G_TYPE_FROM_CLASS(gObjectClass),
                     G_SIGNAL_RUN_LAST,
                     0, 0, 0,
                     g_cclosure_marshal_VOID__UINT,
                     G_TYPE_NONE, 1, G_TYPE_UINT);
}

/**
 * webkit_find_controller_get_search_text:
 * @find_controller: the #WebKitFindController
 *
 * Gets the text that @find_controller is currently searching
 * for. This text is passed to either
 * webkit_find_controller_search() or
 * webkit_find_controller_count_matches().
 *
 * Returns: the text to look for in the #WebKitWebView.
 */
const char* webkit_find_controller_get_search_text(WebKitFindController* findController)
{
    g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);

    return findController->priv->searchText.data();
}

/**
 * webkit_find_controller_get_options:
 * @find_controller: the #WebKitFindController
 *
 * Gets a bitmask containing the #WebKitFindOptions associated with
 * the current search.
 *
 * Returns: a bitmask containing the #WebKitFindOptions associated
 * with the current search.
 */
guint32 webkit_find_controller_get_options(WebKitFindController* findController)
{
    g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), WEBKIT_FIND_OPTIONS_NONE);

    return findController->priv->findOptions;
}

/**
 * webkit_find_controller_get_max_match_count:
 * @find_controller: the #WebKitFindController
 *
 * Gets the maximum number of matches to report during a text
 * lookup. This number is passed as the last argument of
 * webkit_find_controller_search() or
 * webkit_find_controller_count_matches().
 *
 * Returns: the maximum number of matches to report.
 */
guint webkit_find_controller_get_max_match_count(WebKitFindController* findController)
{
    g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);

    return findController->priv->maxMatchCount;
}

/**
 * webkit_find_controller_get_web_view:
 * @find_controller: the #WebKitFindController
 *
 * Gets the #WebKitWebView this find controller is associated to. Do
 * not unref the returned instance as it belongs to the
 * #WebKitFindController.
 *
 * Returns: (transfer none): the #WebKitWebView.
 */
WebKitWebView* webkit_find_controller_get_web_view(WebKitFindController* findController)
{
    g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);

    return findController->priv->webView;
}

static void webKitFindControllerPerform(WebKitFindController* findController, WebKitFindControllerOperation operation)
{
    WKFindOptions wkFindOptions = static_cast<WKFindOptions>(findController->priv->findOptions);
    WKRetainPtr<WKStringRef> wkSearchText(AdoptWK, WKStringCreateWithUTF8CString(findController->priv->searchText.data()));
    WKPageRef wkPage = getWKPageFromWebKitWebView(findController->priv->webView);

    if (operation == FindOperation) {
        // Unconditionally highlight text matches. WK1 API was forcing
        // clients to enable/disable highlighting. Since most of them
        // (all?) where using highlighting we decided to simplify the
        // WK2 API and unconditionally show highlights.
        wkFindOptions = static_cast<WKFindOptions>(findController->priv->findOptions | kWKFindOptionsShowHighlight);
        WKPageFindString(wkPage, wkSearchText.get(), wkFindOptions, findController->priv->maxMatchCount);
        return;
    }

    WKPageCountStringMatches(wkPage, wkSearchText.get(), wkFindOptions, findController->priv->maxMatchCount);
}

static inline void webKitFindControllerSetSearchData(WebKitFindController* findController, const gchar* searchText, guint32 findOptions, guint maxMatchCount)
{
    findController->priv->searchText = searchText;
    findController->priv->findOptions = findOptions;
    findController->priv->maxMatchCount = maxMatchCount;
}

/**
 * webkit_find_controller_search:
 * @find_controller: the #WebKitFindController
 * @search_text: the text to look for
 * @find_options: a bitmask with the #WebKitFindOptions used in the search
 * @max_match_count: the maximum number of matches allowed in the search
 *
 * Looks for @search_text in the #WebKitWebView associated with
 * @find_controller since the beginning of the document highlighting
 * up to @max_match_count matches. The outcome of the search will be
 * asynchronously provided by the #WebKitFindController::found-text
 * and #WebKitFindController::failed-to-find-text signals.
 *
 * To look for the next or previous occurrences of the same text
 * with the same find options use webkit_find_controller_search_next()
 * and/or webkit_find_controller_search_previous(). The
 * #WebKitFindController will use the same text and options for the
 * following searches unless they are modified by another call to this
 * method.
 *
 * Note that if the number of matches is higher than @max_match_count
 * then #WebKitFindController::found-text will report %G_MAXUINT matches
 * instead of the actual number.
 *
 * Callers should call webkit_find_controller_search_finish() to
 * finish the current search operation.
 */
void webkit_find_controller_search(WebKitFindController* findController, const gchar* searchText, guint findOptions, guint maxMatchCount)
{
    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
    g_return_if_fail(searchText);

    webKitFindControllerSetSearchData(findController, searchText, findOptions, maxMatchCount);
    webKitFindControllerPerform(findController, FindOperation);
}

/**
 * webkit_find_controller_search_next:
 * @find_controller: the #WebKitFindController
 *
 * Looks for the next occurrence of the search text.
 *
 * Calling this method before webkit_find_controller_search() or
 * webkit_find_controller_count_matches() is a programming error.
 */
void webkit_find_controller_search_next(WebKitFindController* findController)
{
    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));

    findController->priv->findOptions = findController->priv->findOptions & ~WEBKIT_FIND_OPTIONS_BACKWARDS;
    webKitFindControllerPerform(findController, FindOperation);
}

/**
 * webkit_find_controller_search_previous:
 * @find_controller: the #WebKitFindController
 *
 * Looks for the previous occurrence of the search text.
 *
 * Calling this method before webkit_find_controller_search() or
 * webkit_find_controller_count_matches() is a programming error.
 */
void webkit_find_controller_search_previous(WebKitFindController* findController)
{
    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));

    findController->priv->findOptions = findController->priv->findOptions | WEBKIT_FIND_OPTIONS_BACKWARDS;
    webKitFindControllerPerform(findController, FindOperation);
}

/**
 * webkit_find_controller_count_matches:
 * @find_controller: the #WebKitFindController
 * @search_text: the text to look for
 * @find_options: a bitmask with the #WebKitFindOptions used in the search
 * @max_match_count: the maximum number of matches allowed in the search
 *
 * Counts the number of matches for @search_text found in the
 * #WebKitWebView with the provided @find_options. The number of
 * matches will be provided by the
 * #WebKitFindController::counted-matches signal.
 */
void webkit_find_controller_count_matches(WebKitFindController* findController, const gchar* searchText, guint32 findOptions, guint maxMatchCount)
{
    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
    g_return_if_fail(searchText);

    webKitFindControllerSetSearchData(findController, searchText, findOptions, maxMatchCount);
    webKitFindControllerPerform(findController, CountOperation);
}

/**
 * webkit_find_controller_search_finish:
 * @find_controller: a #WebKitFindController
 *
 * Finishes a find operation started by
 * webkit_find_controller_search(). It will basically unhighlight
 * every text match found.
 *
 * This method will be typically called when the search UI is
 * closed/hidden by the client application.
 */
void webkit_find_controller_search_finish(WebKitFindController* findController)
{
    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));

    WKPageHideFindUI(getWKPageFromWebKitWebView(findController->priv->webView));
}