1/*
2 * Copyright (C) 2012 Igalia S.L.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2,1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB.  If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#include "config.h"
21#include "WebKitFindController.h"
22
23#include "WebKitEnumTypes.h"
24#include "WebKitPrivate.h"
25#include "WebKitWebView.h"
26#include "WebKitWebViewBasePrivate.h"
27#include <glib/gi18n-lib.h>
28#include <wtf/gobject/GRefPtr.h>
29#include <wtf/text/CString.h>
30
31using namespace WebKit;
32using namespace WebCore;
33
34/**
35 * SECTION: WebKitFindController
36 * @Short_description: Controls text search in a #WebKitWebView
37 * @Title: WebKitFindController
38 *
39 * A #WebKitFindController is used to search text in a #WebKitWebView. You
40 * can get a #WebKitWebView<!-- -->'s #WebKitFindController with
41 * webkit_web_view_get_find_controller(), and later use it to search
42 * for text using webkit_find_controller_search(), or get the
43 * number of matches using webkit_find_controller_count_matches(). The
44 * operations are asynchronous and trigger signals when ready, such as
45 * #WebKitFindController::found-text,
46 * #WebKitFindController::failed-to-find-text or
47 * #WebKitFindController::counted-matches<!-- -->.
48 *
49 */
50
51enum {
52    FOUND_TEXT,
53    FAILED_TO_FIND_TEXT,
54    COUNTED_MATCHES,
55
56    LAST_SIGNAL
57};
58
59enum {
60    PROP_0,
61
62    PROP_TEXT,
63    PROP_OPTIONS,
64    PROP_MAX_MATCH_COUNT,
65    PROP_WEB_VIEW
66};
67
68typedef enum {
69    FindOperation,
70    FindNextPrevOperation,
71    CountOperation
72} WebKitFindControllerOperation;
73
74struct _WebKitFindControllerPrivate {
75    CString searchText;
76    // Interpreted as WebKit::FindOptions.
77    uint32_t findOptions;
78    unsigned maxMatchCount;
79    WebKitWebView* webView;
80};
81
82static guint signals[LAST_SIGNAL] = { 0, };
83
84WEBKIT_DEFINE_TYPE(WebKitFindController, webkit_find_controller, G_TYPE_OBJECT)
85
86static inline WebKit::FindOptions toWebFindOptions(uint32_t findOptions)
87{
88    return static_cast<WebKit::FindOptions>((findOptions & WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE ? FindOptionsCaseInsensitive : 0)
89        | (findOptions & WEBKIT_FIND_OPTIONS_AT_WORD_STARTS ? FindOptionsAtWordStarts : 0)
90        | (findOptions & WEBKIT_FIND_OPTIONS_TREAT_MEDIAL_CAPITAL_AS_WORD_START ? FindOptionsTreatMedialCapitalAsWordStart : 0)
91        | (findOptions & WEBKIT_FIND_OPTIONS_BACKWARDS ? FindOptionsBackwards : 0)
92        | (findOptions & WEBKIT_FIND_OPTIONS_WRAP_AROUND ? FindOptionsWrapAround : 0));
93}
94
95static inline WebKitFindOptions toWebKitFindOptions(uint32_t findOptions)
96{
97    return static_cast<WebKitFindOptions>((findOptions & FindOptionsCaseInsensitive ? WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE : 0)
98        | (findOptions & FindOptionsAtWordStarts ? WEBKIT_FIND_OPTIONS_AT_WORD_STARTS : 0)
99        | (findOptions & FindOptionsTreatMedialCapitalAsWordStart ? WEBKIT_FIND_OPTIONS_TREAT_MEDIAL_CAPITAL_AS_WORD_START : 0)
100        | (findOptions & FindOptionsBackwards ? WEBKIT_FIND_OPTIONS_BACKWARDS : 0)
101        | (findOptions & FindOptionsWrapAround ? WEBKIT_FIND_OPTIONS_WRAP_AROUND : 0));
102}
103
104static void didFindString(WKPageRef, WKStringRef, unsigned matchCount, const void* clientInfo)
105{
106    g_signal_emit(WEBKIT_FIND_CONTROLLER(clientInfo), signals[FOUND_TEXT], 0, matchCount);
107}
108
109static void didFailToFindString(WKPageRef, WKStringRef, const void* clientInfo)
110{
111    g_signal_emit(WEBKIT_FIND_CONTROLLER(clientInfo), signals[FAILED_TO_FIND_TEXT], 0);
112}
113
114static void didCountStringMatches(WKPageRef, WKStringRef, unsigned matchCount, const void* clientInfo)
115{
116    g_signal_emit(WEBKIT_FIND_CONTROLLER(clientInfo), signals[COUNTED_MATCHES], 0, matchCount);
117}
118
119static inline WebPageProxy* getPage(WebKitFindController* findController)
120{
121    return webkitWebViewBaseGetPage(reinterpret_cast<WebKitWebViewBase*>(findController->priv->webView));
122}
123
124static void webkitFindControllerConstructed(GObject* object)
125{
126    WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);
127    WKPageFindClientV0 wkFindClient = {
128        {
129            0, // version
130            findController, // clientInfo
131        },
132        didFindString,
133        didFailToFindString,
134        didCountStringMatches
135    };
136
137    WKPageSetPageFindClient(toAPI(getPage(findController)), &wkFindClient.base);
138}
139
140static void webkitFindControllerGetProperty(GObject* object, guint propId, GValue* value, GParamSpec* paramSpec)
141{
142    WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);
143
144    switch (propId) {
145    case PROP_TEXT:
146        g_value_set_string(value, webkit_find_controller_get_search_text(findController));
147        break;
148    case PROP_OPTIONS:
149        g_value_set_uint(value, webkit_find_controller_get_options(findController));
150        break;
151    case PROP_MAX_MATCH_COUNT:
152        g_value_set_uint(value, webkit_find_controller_get_max_match_count(findController));
153        break;
154    case PROP_WEB_VIEW:
155        g_value_set_object(value, webkit_find_controller_get_web_view(findController));
156        break;
157    default:
158        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
159    }
160}
161
162static void webkitFindControllerSetProperty(GObject* object, guint propId, const GValue* value, GParamSpec* paramSpec)
163{
164    WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);
165
166    switch (propId) {
167    case PROP_WEB_VIEW:
168        findController->priv->webView = WEBKIT_WEB_VIEW(g_value_get_object(value));
169        break;
170    default:
171        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
172    }
173}
174
175static void webkit_find_controller_class_init(WebKitFindControllerClass* findClass)
176{
177    GObjectClass* gObjectClass = G_OBJECT_CLASS(findClass);
178    gObjectClass->constructed = webkitFindControllerConstructed;
179    gObjectClass->get_property = webkitFindControllerGetProperty;
180    gObjectClass->set_property = webkitFindControllerSetProperty;
181
182    /**
183     * WebKitFindController:text:
184     *
185     * The current search text for this #WebKitFindController.
186     */
187    g_object_class_install_property(gObjectClass,
188                                    PROP_TEXT,
189                                    g_param_spec_string("text",
190                                                        _("Search text"),
191                                                        _("Text to search for in the view"),
192                                                        0,
193                                                        WEBKIT_PARAM_READABLE));
194
195    /**
196     * WebKitFindController:options:
197     *
198     * The options to be used in the search operation.
199     */
200    g_object_class_install_property(gObjectClass,
201                                    PROP_OPTIONS,
202                                    g_param_spec_flags("options",
203                                                       _("Search Options"),
204                                                       _("Search options to be used in the search operation"),
205                                                       WEBKIT_TYPE_FIND_OPTIONS,
206                                                       WEBKIT_FIND_OPTIONS_NONE,
207                                                       WEBKIT_PARAM_READABLE));
208
209    /**
210     * WebKitFindController:max-match-count:
211     *
212     * The maximum number of matches to report for a given search.
213     */
214    g_object_class_install_property(gObjectClass,
215                                    PROP_MAX_MATCH_COUNT,
216                                    g_param_spec_uint("max-match-count",
217                                                      _("Maximum matches count"),
218                                                      _("The maximum number of matches in a given text to report"),
219                                                      0, G_MAXUINT, 0,
220                                                      WEBKIT_PARAM_READABLE));
221
222    /**
223     * WebKitFindController:web-view:
224     *
225     * The #WebKitWebView this controller is associated to.
226     */
227    g_object_class_install_property(gObjectClass,
228                                    PROP_WEB_VIEW,
229                                    g_param_spec_object("web-view",
230                                                        _("WebView"),
231                                                        _("The WebView associated with this find controller"),
232                                                        WEBKIT_TYPE_WEB_VIEW,
233                                                        static_cast<GParamFlags>(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));
234
235    /**
236     * WebKitFindController::found-text:
237     * @find_controller: the #WebKitFindController
238     * @match_count: the number of matches found of the search text
239     *
240     * This signal is emitted when a given text is found in the web
241     * page text. It will be issued if the text is found
242     * asynchronously after a call to webkit_find_controller_search(),
243     * webkit_find_controller_search_next() or
244     * webkit_find_controller_search_previous().
245     */
246    signals[FOUND_TEXT] =
247        g_signal_new("found-text",
248                     G_TYPE_FROM_CLASS(gObjectClass),
249                     G_SIGNAL_RUN_LAST,
250                     0, 0, 0,
251                     g_cclosure_marshal_VOID__UINT,
252                     G_TYPE_NONE, 1, G_TYPE_UINT);
253
254    /**
255     * WebKitFindController::failed-to-find-text:
256     * @find_controller: the #WebKitFindController
257     *
258     * This signal is emitted when a search operation does not find
259     * any result for the given text. It will be issued if the text
260     * is not found asynchronously after a call to
261     * webkit_find_controller_search(), webkit_find_controller_search_next()
262     * or webkit_find_controller_search_previous().
263     */
264    signals[FAILED_TO_FIND_TEXT] =
265        g_signal_new("failed-to-find-text",
266                     G_TYPE_FROM_CLASS(gObjectClass),
267                     G_SIGNAL_RUN_LAST,
268                     0, 0, 0,
269                     g_cclosure_marshal_VOID__VOID,
270                     G_TYPE_NONE, 0);
271
272    /**
273     * WebKitFindController::counted-matches:
274     * @find_controller: the #WebKitFindController
275     * @match_count: the number of matches of the search text
276     *
277     * This signal is emitted when the #WebKitFindController has
278     * counted the number of matches for a given text after a call
279     * to webkit_find_controller_count_matches().
280     */
281    signals[COUNTED_MATCHES] =
282        g_signal_new("counted-matches",
283                     G_TYPE_FROM_CLASS(gObjectClass),
284                     G_SIGNAL_RUN_LAST,
285                     0, 0, 0,
286                     g_cclosure_marshal_VOID__UINT,
287                     G_TYPE_NONE, 1, G_TYPE_UINT);
288}
289
290/**
291 * webkit_find_controller_get_search_text:
292 * @find_controller: the #WebKitFindController
293 *
294 * Gets the text that @find_controller is currently searching
295 * for. This text is passed to either
296 * webkit_find_controller_search() or
297 * webkit_find_controller_count_matches().
298 *
299 * Returns: the text to look for in the #WebKitWebView.
300 */
301const char* webkit_find_controller_get_search_text(WebKitFindController* findController)
302{
303    g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);
304
305    return findController->priv->searchText.data();
306}
307
308/**
309 * webkit_find_controller_get_options:
310 * @find_controller: the #WebKitFindController
311 *
312 * Gets a bitmask containing the #WebKitFindOptions associated with
313 * the current search.
314 *
315 * Returns: a bitmask containing the #WebKitFindOptions associated
316 * with the current search.
317 */
318guint32 webkit_find_controller_get_options(WebKitFindController* findController)
319{
320    g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), WEBKIT_FIND_OPTIONS_NONE);
321
322    return toWebKitFindOptions(findController->priv->findOptions);
323}
324
325/**
326 * webkit_find_controller_get_max_match_count:
327 * @find_controller: the #WebKitFindController
328 *
329 * Gets the maximum number of matches to report during a text
330 * lookup. This number is passed as the last argument of
331 * webkit_find_controller_search() or
332 * webkit_find_controller_count_matches().
333 *
334 * Returns: the maximum number of matches to report.
335 */
336guint webkit_find_controller_get_max_match_count(WebKitFindController* findController)
337{
338    g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);
339
340    return findController->priv->maxMatchCount;
341}
342
343/**
344 * webkit_find_controller_get_web_view:
345 * @find_controller: the #WebKitFindController
346 *
347 * Gets the #WebKitWebView this find controller is associated to. Do
348 * not unref the returned instance as it belongs to the
349 * #WebKitFindController.
350 *
351 * Returns: (transfer none): the #WebKitWebView.
352 */
353WebKitWebView* webkit_find_controller_get_web_view(WebKitFindController* findController)
354{
355    g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);
356
357    return findController->priv->webView;
358}
359
360static void webKitFindControllerPerform(WebKitFindController* findController, WebKitFindControllerOperation operation)
361{
362    WebKitFindControllerPrivate* priv = findController->priv;
363    if (operation == CountOperation) {
364        getPage(findController)->countStringMatches(String::fromUTF8(priv->searchText.data()),
365            static_cast<WebKit::FindOptions>(priv->findOptions), priv->maxMatchCount);
366        return;
367    }
368
369    uint32_t findOptions = priv->findOptions;
370    if (operation == FindOperation)
371        // Unconditionally highlight text matches when the search
372        // starts. WK1 API was forcing clients to enable/disable
373        // highlighting. Since most of them (all?) where using that
374        // feature we decided to simplify the WK2 API and
375        // unconditionally show highlights. Both search_next() and
376        // search_prev() should not enable highlighting to avoid an
377        // extra unmarkAllTextMatches() + markAllTextMatches()
378        findOptions |= FindOptionsShowHighlight;
379
380    getPage(findController)->findString(String::fromUTF8(priv->searchText.data()), static_cast<WebKit::FindOptions>(findOptions),
381                                        priv->maxMatchCount);
382}
383
384static inline void webKitFindControllerSetSearchData(WebKitFindController* findController, const gchar* searchText, guint32 findOptions, guint maxMatchCount)
385{
386    findController->priv->searchText = searchText;
387    findController->priv->findOptions = findOptions;
388    findController->priv->maxMatchCount = maxMatchCount;
389}
390
391/**
392 * webkit_find_controller_search:
393 * @find_controller: the #WebKitFindController
394 * @search_text: the text to look for
395 * @find_options: a bitmask with the #WebKitFindOptions used in the search
396 * @max_match_count: the maximum number of matches allowed in the search
397 *
398 * Looks for @search_text in the #WebKitWebView associated with
399 * @find_controller since the beginning of the document highlighting
400 * up to @max_match_count matches. The outcome of the search will be
401 * asynchronously provided by the #WebKitFindController::found-text
402 * and #WebKitFindController::failed-to-find-text signals.
403 *
404 * To look for the next or previous occurrences of the same text
405 * with the same find options use webkit_find_controller_search_next()
406 * and/or webkit_find_controller_search_previous(). The
407 * #WebKitFindController will use the same text and options for the
408 * following searches unless they are modified by another call to this
409 * method.
410 *
411 * Note that if the number of matches is higher than @max_match_count
412 * then #WebKitFindController::found-text will report %G_MAXUINT matches
413 * instead of the actual number.
414 *
415 * Callers should call webkit_find_controller_search_finish() to
416 * finish the current search operation.
417 */
418void webkit_find_controller_search(WebKitFindController* findController, const gchar* searchText, guint findOptions, guint maxMatchCount)
419{
420    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
421    g_return_if_fail(searchText);
422    webKitFindControllerSetSearchData(findController, searchText, toWebFindOptions(findOptions), maxMatchCount);
423    webKitFindControllerPerform(findController, FindOperation);
424}
425
426/**
427 * webkit_find_controller_search_next:
428 * @find_controller: the #WebKitFindController
429 *
430 * Looks for the next occurrence of the search text.
431 *
432 * Calling this method before webkit_find_controller_search() or
433 * webkit_find_controller_count_matches() is a programming error.
434 */
435void webkit_find_controller_search_next(WebKitFindController* findController)
436{
437    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
438
439    findController->priv->findOptions &= ~FindOptionsBackwards;
440    findController->priv->findOptions &= ~FindOptionsShowHighlight;
441    webKitFindControllerPerform(findController, FindNextPrevOperation);
442}
443
444/**
445 * webkit_find_controller_search_previous:
446 * @find_controller: the #WebKitFindController
447 *
448 * Looks for the previous occurrence of the search text.
449 *
450 * Calling this method before webkit_find_controller_search() or
451 * webkit_find_controller_count_matches() is a programming error.
452 */
453void webkit_find_controller_search_previous(WebKitFindController* findController)
454{
455    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
456
457    findController->priv->findOptions |= FindOptionsBackwards;
458    findController->priv->findOptions &= ~FindOptionsShowHighlight;
459    webKitFindControllerPerform(findController, FindNextPrevOperation);
460}
461
462/**
463 * webkit_find_controller_count_matches:
464 * @find_controller: the #WebKitFindController
465 * @search_text: the text to look for
466 * @find_options: a bitmask with the #WebKitFindOptions used in the search
467 * @max_match_count: the maximum number of matches allowed in the search
468 *
469 * Counts the number of matches for @search_text found in the
470 * #WebKitWebView with the provided @find_options. The number of
471 * matches will be provided by the
472 * #WebKitFindController::counted-matches signal.
473 */
474void webkit_find_controller_count_matches(WebKitFindController* findController, const gchar* searchText, guint32 findOptions, guint maxMatchCount)
475{
476    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
477    g_return_if_fail(searchText);
478
479    webKitFindControllerSetSearchData(findController, searchText, toWebFindOptions(findOptions), maxMatchCount);
480    webKitFindControllerPerform(findController, CountOperation);
481}
482
483/**
484 * webkit_find_controller_search_finish:
485 * @find_controller: a #WebKitFindController
486 *
487 * Finishes a find operation started by
488 * webkit_find_controller_search(). It will basically unhighlight
489 * every text match found.
490 *
491 * This method will be typically called when the search UI is
492 * closed/hidden by the client application.
493 */
494void webkit_find_controller_search_finish(WebKitFindController* findController)
495{
496    g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
497
498    getPage(findController)->hideFindUI();
499}
500