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 "APIFindClient.h"
24#include "WebKitEnumTypes.h"
25#include "WebKitWebViewPrivate.h"
26#include <glib/gi18n-lib.h>
27#include <wtf/glib/GRefPtr.h>
28#include <wtf/glib/WTFGType.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 inline WebPageProxy& getPage(WebKitFindController* findController)
105{
106 return webkitWebViewGetPage(findController->priv->webView);
107}
108
109class FindClient final : public API::FindClient {
110public:
111 explicit FindClient(WebKitFindController* findController)
112 : m_findController(findController)
113 {
114 }
115
116private:
117 void didCountStringMatches(WebPageProxy*, const String&, uint32_t matchCount) override
118 {
119 g_signal_emit(m_findController, signals[COUNTED_MATCHES], 0, matchCount);
120 }
121
122 void didFindString(WebPageProxy*, const String&, const Vector<IntRect>&, uint32_t matchCount, int32_t, bool /*didWrapAround*/) override
123 {
124 g_signal_emit(m_findController, signals[FOUND_TEXT], 0, matchCount);
125 }
126
127 void didFailToFindString(WebPageProxy*, const String&) override
128 {
129 g_signal_emit(m_findController, signals[FAILED_TO_FIND_TEXT], 0);
130 }
131
132 WebKitFindController* m_findController;
133};
134
135static void webkitFindControllerConstructed(GObject* object)
136{
137 G_OBJECT_CLASS(webkit_find_controller_parent_class)->constructed(object);
138
139 WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);
140 getPage(findController).setFindClient(std::make_unique<FindClient>(findController));
141}
142
143static void webkitFindControllerGetProperty(GObject* object, guint propId, GValue* value, GParamSpec* paramSpec)
144{
145 WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);
146
147 switch (propId) {
148 case PROP_TEXT:
149 g_value_set_string(value, webkit_find_controller_get_search_text(findController));
150 break;
151 case PROP_OPTIONS:
152 g_value_set_uint(value, webkit_find_controller_get_options(findController));
153 break;
154 case PROP_MAX_MATCH_COUNT:
155 g_value_set_uint(value, webkit_find_controller_get_max_match_count(findController));
156 break;
157 case PROP_WEB_VIEW:
158 g_value_set_object(value, webkit_find_controller_get_web_view(findController));
159 break;
160 default:
161 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
162 }
163}
164
165static void webkitFindControllerSetProperty(GObject* object, guint propId, const GValue* value, GParamSpec* paramSpec)
166{
167 WebKitFindController* findController = WEBKIT_FIND_CONTROLLER(object);
168
169 switch (propId) {
170 case PROP_WEB_VIEW:
171 findController->priv->webView = WEBKIT_WEB_VIEW(g_value_get_object(value));
172 break;
173 default:
174 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, paramSpec);
175 }
176}
177
178static void webkit_find_controller_class_init(WebKitFindControllerClass* findClass)
179{
180 GObjectClass* gObjectClass = G_OBJECT_CLASS(findClass);
181 gObjectClass->constructed = webkitFindControllerConstructed;
182 gObjectClass->get_property = webkitFindControllerGetProperty;
183 gObjectClass->set_property = webkitFindControllerSetProperty;
184
185 /**
186 * WebKitFindController:text:
187 *
188 * The current search text for this #WebKitFindController.
189 */
190 g_object_class_install_property(gObjectClass,
191 PROP_TEXT,
192 g_param_spec_string("text",
193 _("Search text"),
194 _("Text to search for in the view"),
195 0,
196 WEBKIT_PARAM_READABLE));
197
198 /**
199 * WebKitFindController:options:
200 *
201 * The options to be used in the search operation.
202 */
203 g_object_class_install_property(gObjectClass,
204 PROP_OPTIONS,
205 g_param_spec_flags("options",
206 _("Search Options"),
207 _("Search options to be used in the search operation"),
208 WEBKIT_TYPE_FIND_OPTIONS,
209 WEBKIT_FIND_OPTIONS_NONE,
210 WEBKIT_PARAM_READABLE));
211
212 /**
213 * WebKitFindController:max-match-count:
214 *
215 * The maximum number of matches to report for a given search.
216 */
217 g_object_class_install_property(gObjectClass,
218 PROP_MAX_MATCH_COUNT,
219 g_param_spec_uint("max-match-count",
220 _("Maximum matches count"),
221 _("The maximum number of matches in a given text to report"),
222 0, G_MAXUINT, 0,
223 WEBKIT_PARAM_READABLE));
224
225 /**
226 * WebKitFindController:web-view:
227 *
228 * The #WebKitWebView this controller is associated to.
229 */
230 g_object_class_install_property(gObjectClass,
231 PROP_WEB_VIEW,
232 g_param_spec_object("web-view",
233 _("WebView"),
234 _("The WebView associated with this find controller"),
235 WEBKIT_TYPE_WEB_VIEW,
236 static_cast<GParamFlags>(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));
237
238 /**
239 * WebKitFindController::found-text:
240 * @find_controller: the #WebKitFindController
241 * @match_count: the number of matches found of the search text
242 *
243 * This signal is emitted when a given text is found in the web
244 * page text. It will be issued if the text is found
245 * asynchronously after a call to webkit_find_controller_search(),
246 * webkit_find_controller_search_next() or
247 * webkit_find_controller_search_previous().
248 */
249 signals[FOUND_TEXT] =
250 g_signal_new("found-text",
251 G_TYPE_FROM_CLASS(gObjectClass),
252 G_SIGNAL_RUN_LAST,
253 0, 0, 0,
254 g_cclosure_marshal_VOID__UINT,
255 G_TYPE_NONE, 1, G_TYPE_UINT);
256
257 /**
258 * WebKitFindController::failed-to-find-text:
259 * @find_controller: the #WebKitFindController
260 *
261 * This signal is emitted when a search operation does not find
262 * any result for the given text. It will be issued if the text
263 * is not found asynchronously after a call to
264 * webkit_find_controller_search(), webkit_find_controller_search_next()
265 * or webkit_find_controller_search_previous().
266 */
267 signals[FAILED_TO_FIND_TEXT] =
268 g_signal_new("failed-to-find-text",
269 G_TYPE_FROM_CLASS(gObjectClass),
270 G_SIGNAL_RUN_LAST,
271 0, 0, 0,
272 g_cclosure_marshal_VOID__VOID,
273 G_TYPE_NONE, 0);
274
275 /**
276 * WebKitFindController::counted-matches:
277 * @find_controller: the #WebKitFindController
278 * @match_count: the number of matches of the search text
279 *
280 * This signal is emitted when the #WebKitFindController has
281 * counted the number of matches for a given text after a call
282 * to webkit_find_controller_count_matches().
283 */
284 signals[COUNTED_MATCHES] =
285 g_signal_new("counted-matches",
286 G_TYPE_FROM_CLASS(gObjectClass),
287 G_SIGNAL_RUN_LAST,
288 0, 0, 0,
289 g_cclosure_marshal_VOID__UINT,
290 G_TYPE_NONE, 1, G_TYPE_UINT);
291}
292
293/**
294 * webkit_find_controller_get_search_text:
295 * @find_controller: the #WebKitFindController
296 *
297 * Gets the text that @find_controller is currently searching
298 * for. This text is passed to either
299 * webkit_find_controller_search() or
300 * webkit_find_controller_count_matches().
301 *
302 * Returns: the text to look for in the #WebKitWebView.
303 */
304const char* webkit_find_controller_get_search_text(WebKitFindController* findController)
305{
306 g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);
307
308 return findController->priv->searchText.data();
309}
310
311/**
312 * webkit_find_controller_get_options:
313 * @find_controller: the #WebKitFindController
314 *
315 * Gets a bitmask containing the #WebKitFindOptions associated with
316 * the current search.
317 *
318 * Returns: a bitmask containing the #WebKitFindOptions associated
319 * with the current search.
320 */
321guint32 webkit_find_controller_get_options(WebKitFindController* findController)
322{
323 g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), WEBKIT_FIND_OPTIONS_NONE);
324
325 return toWebKitFindOptions(findController->priv->findOptions);
326}
327
328/**
329 * webkit_find_controller_get_max_match_count:
330 * @find_controller: the #WebKitFindController
331 *
332 * Gets the maximum number of matches to report during a text
333 * lookup. This number is passed as the last argument of
334 * webkit_find_controller_search() or
335 * webkit_find_controller_count_matches().
336 *
337 * Returns: the maximum number of matches to report.
338 */
339guint webkit_find_controller_get_max_match_count(WebKitFindController* findController)
340{
341 g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);
342
343 return findController->priv->maxMatchCount;
344}
345
346/**
347 * webkit_find_controller_get_web_view:
348 * @find_controller: the #WebKitFindController
349 *
350 * Gets the #WebKitWebView this find controller is associated to. Do
351 * not dereference the returned instance as it belongs to the
352 * #WebKitFindController.
353 *
354 * Returns: (transfer none): the #WebKitWebView.
355 */
356WebKitWebView* webkit_find_controller_get_web_view(WebKitFindController* findController)
357{
358 g_return_val_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController), 0);
359
360 return findController->priv->webView;
361}
362
363static void webKitFindControllerPerform(WebKitFindController* findController, WebKitFindControllerOperation operation)
364{
365 WebKitFindControllerPrivate* priv = findController->priv;
366 if (operation == CountOperation) {
367 getPage(findController).countStringMatches(String::fromUTF8(priv->searchText.data()),
368 static_cast<WebKit::FindOptions>(priv->findOptions), priv->maxMatchCount);
369 return;
370 }
371
372 uint32_t findOptions = priv->findOptions;
373 if (operation == FindOperation)
374 // Unconditionally highlight text matches when the search
375 // starts. WK1 API was forcing clients to enable/disable
376 // highlighting. Since most of them (all?) where using that
377 // feature we decided to simplify the WK2 API and
378 // unconditionally show highlights. Both search_next() and
379 // search_prev() should not enable highlighting to avoid an
380 // extra unmarkAllTextMatches() + markAllTextMatches()
381 findOptions |= FindOptionsShowHighlight;
382
383 getPage(findController).findString(String::fromUTF8(priv->searchText.data()), static_cast<WebKit::FindOptions>(findOptions), priv->maxMatchCount);
384}
385
386static inline void webKitFindControllerSetSearchData(WebKitFindController* findController, const gchar* searchText, guint32 findOptions, guint maxMatchCount)
387{
388 findController->priv->searchText = searchText;
389 findController->priv->findOptions = findOptions;
390 findController->priv->maxMatchCount = maxMatchCount;
391}
392
393/**
394 * webkit_find_controller_search:
395 * @find_controller: the #WebKitFindController
396 * @search_text: the text to look for
397 * @find_options: a bitmask with the #WebKitFindOptions used in the search
398 * @max_match_count: the maximum number of matches allowed in the search
399 *
400 * Looks for @search_text in the #WebKitWebView associated with
401 * @find_controller since the beginning of the document highlighting
402 * up to @max_match_count matches. The outcome of the search will be
403 * asynchronously provided by the #WebKitFindController::found-text
404 * and #WebKitFindController::failed-to-find-text signals.
405 *
406 * To look for the next or previous occurrences of the same text
407 * with the same find options use webkit_find_controller_search_next()
408 * and/or webkit_find_controller_search_previous(). The
409 * #WebKitFindController will use the same text and options for the
410 * following searches unless they are modified by another call to this
411 * method.
412 *
413 * Note that if the number of matches is higher than @max_match_count
414 * then #WebKitFindController::found-text will report %G_MAXUINT matches
415 * instead of the actual number.
416 *
417 * Callers should call webkit_find_controller_search_finish() to
418 * finish the current search operation.
419 */
420void webkit_find_controller_search(WebKitFindController* findController, const gchar* searchText, guint findOptions, guint maxMatchCount)
421{
422 g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
423 g_return_if_fail(searchText);
424 webKitFindControllerSetSearchData(findController, searchText, toWebFindOptions(findOptions), maxMatchCount);
425 webKitFindControllerPerform(findController, FindOperation);
426}
427
428/**
429 * webkit_find_controller_search_next:
430 * @find_controller: the #WebKitFindController
431 *
432 * Looks for the next occurrence of the search text.
433 *
434 * Calling this method before webkit_find_controller_search() or
435 * webkit_find_controller_count_matches() is a programming error.
436 */
437void webkit_find_controller_search_next(WebKitFindController* findController)
438{
439 g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
440
441 findController->priv->findOptions &= ~FindOptionsBackwards;
442 findController->priv->findOptions &= ~FindOptionsShowHighlight;
443 webKitFindControllerPerform(findController, FindNextPrevOperation);
444}
445
446/**
447 * webkit_find_controller_search_previous:
448 * @find_controller: the #WebKitFindController
449 *
450 * Looks for the previous occurrence of the search text.
451 *
452 * Calling this method before webkit_find_controller_search() or
453 * webkit_find_controller_count_matches() is a programming error.
454 */
455void webkit_find_controller_search_previous(WebKitFindController* findController)
456{
457 g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
458
459 findController->priv->findOptions |= FindOptionsBackwards;
460 findController->priv->findOptions &= ~FindOptionsShowHighlight;
461 webKitFindControllerPerform(findController, FindNextPrevOperation);
462}
463
464/**
465 * webkit_find_controller_count_matches:
466 * @find_controller: the #WebKitFindController
467 * @search_text: the text to look for
468 * @find_options: a bitmask with the #WebKitFindOptions used in the search
469 * @max_match_count: the maximum number of matches allowed in the search
470 *
471 * Counts the number of matches for @search_text found in the
472 * #WebKitWebView with the provided @find_options. The number of
473 * matches will be provided by the
474 * #WebKitFindController::counted-matches signal.
475 */
476void webkit_find_controller_count_matches(WebKitFindController* findController, const gchar* searchText, guint32 findOptions, guint maxMatchCount)
477{
478 g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
479 g_return_if_fail(searchText);
480
481 webKitFindControllerSetSearchData(findController, searchText, toWebFindOptions(findOptions), maxMatchCount);
482 webKitFindControllerPerform(findController, CountOperation);
483}
484
485/**
486 * webkit_find_controller_search_finish:
487 * @find_controller: a #WebKitFindController
488 *
489 * Finishes a find operation started by
490 * webkit_find_controller_search(). It will basically unhighlight
491 * every text match found.
492 *
493 * This method will be typically called when the search UI is
494 * closed/hidden by the client application.
495 */
496void webkit_find_controller_search_finish(WebKitFindController* findController)
497{
498 g_return_if_fail(WEBKIT_IS_FIND_CONTROLLER(findController));
499
500 getPage(findController).hideFindUI();
501}
502