1 /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are met:
5
6 1. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
8 2. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23 #include "lookupwidget.h"
24
25 #include "app.h"
26 #include "bookmarks.h"
27 #include "command.h"
28 #include "documentwidget.h"
29 #include "feeds.h"
30 #include "gmcerts.h"
31 #include "gmutil.h"
32 #include "history.h"
33 #include "inputwidget.h"
34 #include "listwidget.h"
35 #include "lang.h"
36 #include "lookup.h"
37 #include "util.h"
38 #include "visited.h"
39
40 #if defined (iPlatformAppleMobile)
41 # include "../ios.h"
42 #endif
43
44 #include <the_Foundation/mutex.h>
45 #include <the_Foundation/thread.h>
46 #include <the_Foundation/regexp.h>
47
48 iDeclareType(LookupJob)
49
50 struct Impl_LookupJob {
51 iRegExp *term;
52 iTime now;
53 iObjectList *docs;
54 iPtrArray results;
55 };
56
init_LookupJob(iLookupJob * d)57 static void init_LookupJob(iLookupJob *d) {
58 d->term = NULL;
59 initCurrent_Time(&d->now);
60 d->docs = NULL;
61 init_PtrArray(&d->results);
62 }
63
deinit_LookupJob(iLookupJob * d)64 static void deinit_LookupJob(iLookupJob *d) {
65 iForEach(PtrArray, i, &d->results) {
66 delete_LookupResult(i.ptr);
67 }
68 deinit_PtrArray(&d->results);
69 iRelease(d->docs);
70 iRelease(d->term);
71 }
72
73 iDefineTypeConstruction(LookupJob)
74
75 /*----------------------------------------------------------------------------------------------*/
76
77 iDeclareType(LookupItem)
78 typedef iListItemClass iLookupItemClass;
79
80 struct Impl_LookupItem {
81 iListItem listItem;
82 iLookupResult *result;
83 int font;
84 int fg;
85 iString icon;
86 iString text;
87 iString command;
88 };
89
init_LookupItem(iLookupItem * d,const iLookupResult * res)90 static void init_LookupItem(iLookupItem *d, const iLookupResult *res) {
91 init_ListItem(&d->listItem);
92 d->result = res ? copy_LookupResult(res) : NULL;
93 d->font = uiContent_FontId;
94 d->fg = uiText_ColorId;
95 init_String(&d->icon);
96 if (res && res->icon) {
97 appendChar_String(&d->icon, res->icon);
98 }
99 init_String(&d->text);
100 init_String(&d->command);
101 }
102
deinit_LookupItem(iLookupItem * d)103 static void deinit_LookupItem(iLookupItem *d) {
104 deinit_String(&d->command);
105 deinit_String(&d->text);
106 deinit_String(&d->icon);
107 delete_LookupResult(d->result);
108 }
109
draw_LookupItem_(iLookupItem * d,iPaint * p,iRect rect,const iListWidget * list)110 static void draw_LookupItem_(iLookupItem *d, iPaint *p, iRect rect, const iListWidget *list) {
111 const iBool isPressing = isMouseDown_ListWidget(list);
112 const iBool isHover = isHover_Widget(list) && constHoverItem_ListWidget(list) == d;
113 const iBool isCursor = d->listItem.isSelected;
114 if (isHover || isCursor) {
115 fillRect_Paint(p,
116 rect,
117 isPressing || isCursor ? uiBackgroundPressed_ColorId
118 : uiBackgroundFramelessHover_ColorId);
119 }
120 int fg = isHover || isCursor
121 ? permanent_ColorId | (isPressing || isCursor ? uiTextPressed_ColorId
122 : uiTextFramelessHover_ColorId)
123 : d->fg;
124 const iInt2 size = measureRange_Text(d->font, range_String(&d->text)).bounds.size;
125 iInt2 pos = init_I2(left_Rect(rect) + 3 * gap_UI, mid_Rect(rect).y - size.y / 2);
126 if (d->listItem.isSeparator) {
127 pos.y = bottom_Rect(rect) - lineHeight_Text(d->font);
128 }
129 if (!isEmpty_String(&d->icon)) {
130 const iRect iconRect = { init_I2(pos.x, top_Rect(rect)),
131 init_I2(gap_UI * 5, height_Rect(rect)) };
132 const iRect iconVis = visualBounds_Text(d->font, range_String(&d->icon));
133 drawRange_Text(d->font,
134 sub_I2(mid_Rect(iconRect), mid_Rect(iconVis)),
135 fg,
136 range_String(&d->icon));
137 pos.x += width_Rect(iconRect) + gap_UI * 3 / 2;
138 }
139 drawRange_Text(d->font, pos, fg, range_String(&d->text));
140 }
141
142 iBeginDefineSubclass(LookupItem, ListItem)
143 .draw = (iAny *) draw_LookupItem_,
144 iEndDefineSubclass(LookupItem)
145
146 iDefineObjectConstructionArgs(LookupItem, (const iLookupResult *res), res)
147
148 /*----------------------------------------------------------------------------------------------*/
149
150 struct Impl_LookupWidget {
151 iWidget widget;
152 iListWidget *list;
153 size_t cursor;
154 iThread * work;
155 iCondition jobAvailable; /* wakes up the work thread */
156 iMutex * mtx;
157 iString pendingTerm;
158 iObjectList *pendingDocs;
159 iLookupJob * finishedJob;
160 };
161
scoreMatch_(const iRegExp * pattern,iRangecc text)162 static float scoreMatch_(const iRegExp *pattern, iRangecc text) {
163 float score = 0.0f;
164 iRegExpMatch m;
165 init_RegExpMatch(&m);
166 while (matchRange_RegExp(pattern, text, &m)) {
167 /* Match near the beginning is scored higher. */
168 score += (float) size_Range(&m.range) / ((float) m.range.start + 1);
169 }
170 return score;
171 }
172
bookmarkRelevance_LookupJob_(const iLookupJob * d,const iBookmark * bm)173 static float bookmarkRelevance_LookupJob_(const iLookupJob *d, const iBookmark *bm) {
174 if (isFolder_Bookmark(bm)) {
175 return 0.0f;
176 }
177 iUrl parts;
178 init_Url(&parts, &bm->url);
179 const float t = scoreMatch_(d->term, range_String(&bm->title));
180 const float h = scoreMatch_(d->term, parts.host);
181 const float p = scoreMatch_(d->term, parts.path);
182 const float g = scoreMatch_(d->term, range_String(&bm->tags));
183 return h + iMax(p, t) + 2 * g; /* extra weight for tags */
184 }
185
feedEntryRelevance_LookupJob_(const iLookupJob * d,const iFeedEntry * entry)186 static float feedEntryRelevance_LookupJob_(const iLookupJob *d, const iFeedEntry *entry) {
187 iUrl parts;
188 init_Url(&parts, &entry->url);
189 const float t = scoreMatch_(d->term, range_String(&entry->title));
190 const float h = scoreMatch_(d->term, parts.host);
191 const float p = scoreMatch_(d->term, parts.path);
192 const double age = secondsSince_Time(&d->now, &entry->posted) / 3600.0 / 24.0; /* days */
193 return (t * 3 + h + p) / (age + 1); /* extra weight for title, recency */
194 }
195
identityRelevance_LookupJob_(const iLookupJob * d,const iGmIdentity * identity)196 static float identityRelevance_LookupJob_(const iLookupJob *d, const iGmIdentity *identity) {
197 iString *cn = subject_TlsCertificate(identity->cert);
198 const float c = scoreMatch_(d->term, range_String(cn));
199 const float n = scoreMatch_(d->term, range_String(&identity->notes));
200 delete_String(cn);
201 return c + 2 * n; /* extra weight for notes */
202 }
203
visitedRelevance_LookupJob_(const iLookupJob * d,const iVisitedUrl * vis)204 static float visitedRelevance_LookupJob_(const iLookupJob *d, const iVisitedUrl *vis) {
205 iUrl parts;
206 init_Url(&parts, &vis->url);
207 const float h = scoreMatch_(d->term, parts.host);
208 const float p = scoreMatch_(d->term, parts.path);
209 const double age = secondsSince_Time(&d->now, &vis->when) / 3600.0 / 24.0; /* days */
210 return iMax(h, p) / (age + 1); /* extra weight for recency */
211 }
212
matchBookmark_LookupJob_(void * context,const iBookmark * bm)213 static iBool matchBookmark_LookupJob_(void *context, const iBookmark *bm) {
214 return bookmarkRelevance_LookupJob_(context, bm) > 0;
215 }
216
matchIdentity_LookupJob_(void * context,const iGmIdentity * identity)217 static iBool matchIdentity_LookupJob_(void *context, const iGmIdentity *identity) {
218 return identityRelevance_LookupJob_(context, identity) > 0;
219 }
220
searchBookmarks_LookupJob_(iLookupJob * d)221 static void searchBookmarks_LookupJob_(iLookupJob *d) {
222 /* Note: Called in a background thread. */
223 /* TODO: Thread safety! What if a bookmark gets deleted while its being accessed here? */
224 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), NULL, matchBookmark_LookupJob_, d)) {
225 const iBookmark *bm = i.ptr;
226 iLookupResult * res = new_LookupResult();
227 res->type = bookmark_LookupResultType;
228 res->when = bm->when;
229 res->relevance = bookmarkRelevance_LookupJob_(d, bm);
230 res->icon = bm->icon;
231 set_String(&res->label, &bm->title);
232 set_String(&res->url, &bm->url);
233 pushBack_PtrArray(&d->results, res);
234 }
235 }
236
searchFeeds_LookupJob_(iLookupJob * d)237 static void searchFeeds_LookupJob_(iLookupJob *d) {
238 iConstForEach(PtrArray, i, listEntries_Feeds()) {
239 const iFeedEntry *entry = i.ptr;
240 const iBookmark *bm = get_Bookmarks(bookmarks_App(), entry->bookmarkId);
241 if (!bm) {
242 continue;
243 }
244 const float relevance = feedEntryRelevance_LookupJob_(d, entry);
245 if (relevance > 0) {
246 iLookupResult *res = new_LookupResult();
247 res->type = feedEntry_LookupResultType;
248 res->when = entry->posted;
249 res->relevance = relevance;
250 set_String(&res->url, &entry->url);
251 set_String(&res->meta, &bm->title);
252 set_String(&res->label, &entry->title);
253 res->icon = bm->icon;
254 pushBack_PtrArray(&d->results, res);
255 }
256 }
257 }
258
searchVisited_LookupJob_(iLookupJob * d)259 static void searchVisited_LookupJob_(iLookupJob *d) {
260 /* Note: Called in a background thread. */
261 /* TODO: Thread safety! Visited URLs may be deleted while being accessed here. */
262 iConstForEach(PtrArray, i, list_Visited(visited_App(), 0)) {
263 const iVisitedUrl *vis = i.ptr;
264 const float relevance = visitedRelevance_LookupJob_(d, vis);
265 if (relevance > 0) {
266 iLookupResult *res = new_LookupResult();
267 res->type = history_LookupResultType;
268 res->relevance = relevance;
269 set_String(&res->label, &vis->url);
270 set_String(&res->url, &vis->url);
271 res->when = vis->when;
272 pushBack_PtrArray(&d->results, res);
273 }
274 }
275 }
276
searchHistory_LookupJob_(iLookupJob * d)277 static void searchHistory_LookupJob_(iLookupJob *d) {
278 /* Note: Called in a background thread. */
279 size_t index = 0;
280 iForEach(ObjectList, i, d->docs) {
281 iConstForEach(StringArray, j,
282 searchContents_History(history_DocumentWidget(i.object), d->term)) {
283 const char *match = cstr_String(j.value);
284 const size_t matchLen = argLabel_Command(match, "len");
285 iRangecc text;
286 text.start = strstr(match, " str:") + 5;
287 text.end = text.start + matchLen;
288 const char *url = strstr(text.end, " url:") + 5;
289 iLookupResult *res = new_LookupResult();
290 res->type = content_LookupResultType;
291 res->relevance = ++index; /* most recent comes last */
292 setCStr_String(&res->label, "\"");
293 appendRange_String(&res->label, text);
294 appendCStr_String(&res->label, "\"");
295 setCStr_String(&res->url, url);
296 pushBack_PtrArray(&d->results, res);
297 }
298 }
299 }
300
searchIdentities_LookupJob_(iLookupJob * d)301 static void searchIdentities_LookupJob_(iLookupJob *d) {
302 /* Note: Called in a background thread. */
303 iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), matchIdentity_LookupJob_, d)) {
304 const iGmIdentity *identity = i.ptr;
305 iLookupResult *res = new_LookupResult();
306 res->type = identity_LookupResultType;
307 res->relevance = identityRelevance_LookupJob_(d, identity);
308 res->icon = 0x1f464; /* identity->icon; */
309 iString *cn = subject_TlsCertificate(identity->cert);
310 set_String(&res->label, cn);
311 delete_String(cn);
312 set_String(&res->meta,
313 collect_String(
314 hexEncode_Block(collect_Block(fingerprint_TlsCertificate(identity->cert)))));
315 pushBack_PtrArray(&d->results, res);
316 }
317 }
318
worker_LookupWidget_(iThread * thread)319 static iThreadResult worker_LookupWidget_(iThread *thread) {
320 iLookupWidget *d = userData_Thread(thread);
321 // printf("[LookupWidget] worker is running\n"); fflush(stdout);
322 lock_Mutex(d->mtx);
323 for (;;) {
324 wait_Condition(&d->jobAvailable, d->mtx);
325 if (isEmpty_String(&d->pendingTerm)) {
326 break; /* Time to quit. */
327 }
328 iLookupJob *job = new_LookupJob();
329 /* Make a regular expression to search for multiple alternative words. */ {
330 iString *pattern = new_String();
331 iRangecc word = iNullRange;
332 iBool isFirst = iTrue;
333 iString wordStr;
334 init_String(&wordStr);
335 while (nextSplit_Rangecc(range_String(&d->pendingTerm), " ", &word)) {
336 if (isEmpty_Range(&word)) continue;
337 if (!isFirst) appendCStr_String(pattern, ".*");
338 setRange_String(&wordStr, word);
339 iConstForEach(String, ch, &wordStr) {
340 /* Escape regular expression characters. */
341 if (isSyntaxChar_RegExp(ch.value)) {
342 appendChar_String(pattern, '\\');
343 }
344 appendChar_String(pattern, ch.value);
345 }
346 isFirst = iFalse;
347 }
348 deinit_String(&wordStr);
349 iAssert(!isEmpty_String(pattern));
350 job->term = new_RegExp(cstr_String(pattern), caseInsensitive_RegExpOption);
351 delete_String(pattern);
352 }
353 const size_t termLen = length_String(&d->pendingTerm); /* characters */
354 clear_String(&d->pendingTerm);
355 job->docs = d->pendingDocs;
356 d->pendingDocs = NULL;
357 unlock_Mutex(d->mtx);
358 /* Do the lookup. */ {
359 searchBookmarks_LookupJob_(job);
360 searchFeeds_LookupJob_(job);
361 searchVisited_LookupJob_(job);
362 if (termLen >= 3) {
363 searchHistory_LookupJob_(job);
364 }
365 searchIdentities_LookupJob_(job);
366 }
367 /* Submit the result. */
368 lock_Mutex(d->mtx);
369 if (d->finishedJob) {
370 /* Previous results haven't been taken yet. */
371 delete_LookupJob(d->finishedJob);
372 }
373 // printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results));
374 fflush(stdout);
375 d->finishedJob = job;
376 postCommand_Widget(as_Widget(d), "lookup.ready");
377 }
378 unlock_Mutex(d->mtx);
379 // printf("[LookupWidget] worker has quit\n"); fflush(stdout);
380 return 0;
381 }
382
iDefineObjectConstruction(LookupWidget)383 iDefineObjectConstruction(LookupWidget)
384
385 static void updateMetrics_LookupWidget_(iLookupWidget *d) {
386 setItemHeight_ListWidget(d->list, lineHeight_Text(uiContent_FontId) * 1.333f);
387 }
388
init_LookupWidget(iLookupWidget * d)389 void init_LookupWidget(iLookupWidget *d) {
390 iWidget *w = as_Widget(d);
391 init_Widget(w);
392 setId_Widget(w, "lookup");
393 setFlags_Widget(w, focusable_WidgetFlag, iTrue);
394 #if defined (iPlatformMobile)
395 setFlags_Widget(w, unhittable_WidgetFlag, iTrue);
396 #endif
397 d->list = addChildFlags_Widget(w, iClob(new_ListWidget()),
398 resizeToParentWidth_WidgetFlag |
399 resizeToParentHeight_WidgetFlag);
400 d->cursor = iInvalidPos;
401 d->work = new_Thread(worker_LookupWidget_);
402 setUserData_Thread(d->work, d);
403 init_Condition(&d->jobAvailable);
404 d->mtx = new_Mutex();
405 init_String(&d->pendingTerm);
406 d->pendingDocs = NULL;
407 d->finishedJob = NULL;
408 updateMetrics_LookupWidget_(d);
409 start_Thread(d->work);
410 }
411
deinit_LookupWidget(iLookupWidget * d)412 void deinit_LookupWidget(iLookupWidget *d) {
413 /* Stop the worker. */ {
414 iGuardMutex(d->mtx, {
415 iReleasePtr(&d->pendingDocs);
416 clear_String(&d->pendingTerm);
417 signal_Condition(&d->jobAvailable);
418 });
419 join_Thread(d->work);
420 iRelease(d->work);
421 }
422 delete_LookupJob(d->finishedJob);
423 deinit_String(&d->pendingTerm);
424 delete_Mutex(d->mtx);
425 deinit_Condition(&d->jobAvailable);
426 }
427
submit_LookupWidget(iLookupWidget * d,const iString * term)428 void submit_LookupWidget(iLookupWidget *d, const iString *term) {
429 iGuardMutex(d->mtx, {
430 set_String(&d->pendingTerm, term);
431 trim_String(&d->pendingTerm);
432 iReleasePtr(&d->pendingDocs);
433 if (!isEmpty_String(&d->pendingTerm)) {
434 d->pendingDocs = listDocuments_App(get_Root()); /* holds reference to all open tabs */
435 signal_Condition(&d->jobAvailable);
436 }
437 else {
438 showCollapsed_Widget(as_Widget(d), iFalse);
439 }
440 });
441 }
442
draw_LookupWidget_(const iLookupWidget * d)443 static void draw_LookupWidget_(const iLookupWidget *d) {
444 const iWidget *w = constAs_Widget(d);
445 draw_Widget(w);
446 /* Draw a frame. */ {
447 iPaint p;
448 init_Paint(&p);
449 drawRect_Paint(&p,
450 bounds_Widget(w),
451 isFocused_Widget(w) ? uiInputFrameFocused_ColorId : uiSeparator_ColorId);
452 }
453 }
454
cmpPtr_LookupResult_(const void * p1,const void * p2)455 static int cmpPtr_LookupResult_(const void *p1, const void *p2) {
456 const iLookupResult *a = *(const iLookupResult **) p1;
457 const iLookupResult *b = *(const iLookupResult **) p2;
458 if (a->type != b->type) {
459 return iCmp(a->type, b->type);
460 }
461 if (fabsf(a->relevance - b->relevance) < 0.0001f) {
462 return cmpString_String(&a->url, &b->url);
463 }
464 return -iCmp(a->relevance, b->relevance);
465 }
466
cstr_LookupResultType(enum iLookupResultType d)467 static const char *cstr_LookupResultType(enum iLookupResultType d) {
468 switch (d) {
469 case bookmark_LookupResultType:
470 return "heading.lookup.bookmarks";
471 case feedEntry_LookupResultType:
472 return "heading.lookup.feeds";
473 case history_LookupResultType:
474 return "heading.lookup.history";
475 case content_LookupResultType:
476 return "heading.lookup.pagecontent";
477 case identity_LookupResultType:
478 return "heading.lookup.identities";
479 default:
480 return "heading.lookup.other";
481 }
482 }
483
presentResults_LookupWidget_(iLookupWidget * d)484 static void presentResults_LookupWidget_(iLookupWidget *d) {
485 iLookupJob *job;
486 iGuardMutex(d->mtx, {
487 job = d->finishedJob;
488 d->finishedJob = NULL;
489 });
490 if (!job) return;
491 clear_ListWidget(d->list);
492 sort_Array(&job->results, cmpPtr_LookupResult_);
493 enum iLookupResultType lastType = none_LookupResultType;
494 const size_t maxPerType = 10; /* TODO: Setting? */
495 size_t perType = 0;
496 iConstForEach(PtrArray, i, &job->results) {
497 const iLookupResult *res = i.ptr;
498 if (lastType != res->type) {
499 /* Heading separator. */
500 iLookupItem *item = new_LookupItem(NULL);
501 item->listItem.isSeparator = iTrue;
502 item->fg = uiHeading_ColorId;
503 item->font = uiLabel_FontId;
504 format_String(&item->text, "%s", cstr_Lang(cstr_LookupResultType(res->type)));
505 addItem_ListWidget(d->list, item);
506 iRelease(item);
507 lastType = res->type;
508 perType = 0;
509 }
510 if (perType > maxPerType) {
511 continue;
512 }
513 if (res->type == identity_LookupResultType) {
514 const iString *docUrl = url_DocumentWidget(document_App());
515 iBlock *finger = hexDecode_Rangecc(range_String(&res->meta));
516 const iGmIdentity *ident = findIdentity_GmCerts(certs_App(), finger);
517 /* Sign in/out. */ {
518 const iBool isUsed = isUsedOn_GmIdentity(ident, docUrl);
519 iLookupItem *item = new_LookupItem(res);
520 item->fg = uiText_ColorId;
521 item->font = uiContent_FontId;
522 format_String(&item->text,
523 "%s \u2014 " uiTextStrong_ColorEscape "%s",
524 cstr_String(&res->label),
525 cstr_Lang(isUsed ? "ident.stopuse" : "ident.use"));
526 format_String(&item->command, "ident.sign%s ident:%s url:%s",
527 isUsed ? "out arg:0" : "in", cstr_String(&res->meta), cstr_String(docUrl));
528 addItem_ListWidget(d->list, item);
529 iRelease(item);
530 }
531 if (isUsed_GmIdentity(ident)) {
532 iLookupItem *item = new_LookupItem(res);
533 item->fg = uiText_ColorId;
534 item->font = uiContent_FontId;
535 format_String(&item->text,
536 "%s \u2014 " uiTextStrong_ColorEscape "%s",
537 cstr_String(&res->label),
538 cstr_Lang("ident.stopuse.all"));
539 format_String(&item->command, "ident.signout arg:1 ident:%s", cstr_String(&res->meta));
540 addItem_ListWidget(d->list, item);
541 iRelease(item);
542 }
543 delete_Block(finger);
544 continue;
545 }
546 iLookupItem *item = new_LookupItem(res);
547 const char *url = cstr_String(&res->url);
548 if (startsWithCase_String(&res->url, "gemini://")) {
549 url += 9;
550 }
551 switch (res->type) {
552 case bookmark_LookupResultType: {
553 item->fg = uiTextStrong_ColorId;
554 item->font = uiContent_FontId;
555 format_String(&item->text,
556 "%s %s\u2014 %s",
557 cstr_String(&res->label),
558 uiText_ColorEscape,
559 url);
560 format_String(&item->command, "open url:%s", cstr_String(&res->url));
561 break;
562 }
563 case feedEntry_LookupResultType: {
564 item->fg = uiTextStrong_ColorId;
565 item->font = uiContent_FontId;
566 format_String(&item->text,
567 "%s %s\u2014 %s",
568 cstr_String(&res->label),
569 uiText_ColorEscape,
570 cstr_String(&res->meta));
571 const iString *cmd = feedEntryOpenCommand_String(&res->url, 0);
572 if (cmd) {
573 set_String(&item->command, cmd);
574 }
575 break;
576 }
577 case history_LookupResultType: {
578 item->fg = uiText_ColorId;
579 item->font = uiContent_FontId;
580 format_String(&item->text, "%s \u2014 ", url);
581 append_String(&item->text, collect_String(format_Time(&res->when, "%b %d, %Y")));
582 format_String(&item->command, "open url:%s", cstr_String(&res->url));
583 break;
584 }
585 case content_LookupResultType: {
586 item->fg = uiText_ColorId;
587 item->font = uiContent_FontId;
588 format_String(&item->text, "%s \u2014 %s", url, cstr_String(&res->label));
589 format_String(&item->command, "open url:%s", cstr_String(&res->url));
590 break;
591 }
592 default:
593 break;
594 }
595 addItem_ListWidget(d->list, item);
596 iRelease(item);
597 perType++;
598 }
599 delete_LookupJob(job);
600 /* Re-select the item at the cursor. */
601 if (d->cursor != iInvalidPos) {
602 d->cursor = iMin(d->cursor, numItems_ListWidget(d->list) - 1);
603 ((iListItem *) item_ListWidget(d->list, d->cursor))->isSelected = iTrue;
604 }
605 scrollOffset_ListWidget(d->list, 0);
606 updateVisible_ListWidget(d->list);
607 invalidate_ListWidget(d->list);
608 showCollapsed_Widget(as_Widget(d), numItems_ListWidget(d->list) != 0);
609 }
610
item_LookupWidget_(iLookupWidget * d,size_t index)611 static iLookupItem *item_LookupWidget_(iLookupWidget *d, size_t index) {
612 return item_ListWidget(d->list, index);
613 }
614
setCursor_LookupWidget_(iLookupWidget * d,size_t index)615 static void setCursor_LookupWidget_(iLookupWidget *d, size_t index) {
616 if (index != d->cursor) {
617 iLookupItem *item = item_LookupWidget_(d, d->cursor);
618 if (item) {
619 item->listItem.isSelected = iFalse;
620 invalidateItem_ListWidget(d->list, d->cursor);
621 }
622 d->cursor = index;
623 if ((item = item_LookupWidget_(d, d->cursor)) != NULL) {
624 item->listItem.isSelected = iTrue;
625 invalidateItem_ListWidget(d->list, d->cursor);
626 }
627 scrollToItem_ListWidget(d->list, d->cursor, 0);
628 }
629 }
630
moveCursor_LookupWidget_(iLookupWidget * d,int delta)631 static iBool moveCursor_LookupWidget_(iLookupWidget *d, int delta) {
632 const int dir = iSign(delta);
633 size_t cur = d->cursor;
634 size_t good = cur;
635 while (delta && ((dir < 0 && cur > 0) || (dir > 0 && cur < numItems_ListWidget(d->list) - 1))) {
636 cur += dir;
637 if (!item_LookupWidget_(d, cur)->listItem.isSeparator) {
638 delta -= dir;
639 good = cur;
640 }
641 }
642 setCursor_LookupWidget_(d, good);
643 return delta == 0;
644 }
645
processEvent_LookupWidget_(iLookupWidget * d,const SDL_Event * ev)646 static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {
647 iWidget *w = as_Widget(d);
648 const char *cmd = command_UserEvent(ev);
649 if (isCommand_Widget(w, ev, "lookup.ready")) {
650 /* Take the results and present them in the list. */
651 presentResults_LookupWidget_(d);
652 return iTrue;
653 }
654 if (isMetricsChange_UserEvent(ev)) {
655 updateMetrics_LookupWidget_(d);
656 }
657 else if (isResize_UserEvent(ev) || equal_Command(cmd, "keyboard.changed") ||
658 (equal_Command(cmd, "layout.changed") &&
659 equal_Rangecc(range_Command(cmd, "id"), "navbar"))) {
660 /* Position the lookup popup under the URL bar. */ {
661 iRoot *root = w->root;
662 const iRect navBarBounds = bounds_Widget(findChild_Widget(root->widget, "navbar"));
663 iWidget *url = findChild_Widget(root->widget, "url");
664 setFixedSize_Widget(w, init_I2(width_Widget(url),
665 (bottom_Rect(rect_Root(root)) - bottom_Rect(navBarBounds)) / 2));
666 setPos_Widget(w, windowToLocal_Widget(w, bottomLeft_Rect(bounds_Widget(url))));
667 #if defined (iPlatformAppleMobile)
668 /* Adjust height based on keyboard size. */ {
669 w->rect.size.y = visibleSize_Root(root).y - top_Rect(bounds_Widget(w));
670 if (deviceType_App() == phone_AppDeviceType) {
671 float l, r;
672 safeAreaInsets_iOS(&l, NULL, &r, NULL);
673 w->rect.size.x = size_Root(root).x - l - r;
674 w->rect.pos.x = l;
675 /* TODO: Need to use windowToLocal_Widget? */
676 }
677 }
678 #endif
679 arrange_Widget(w);
680 }
681 updateVisible_ListWidget(d->list);
682 invalidate_ListWidget(d->list);
683 }
684 if (equal_Command(cmd, "input.ended") && equal_Rangecc(range_Command(cmd, "id"), "url") &&
685 !isFocused_Widget(w)) {
686 showCollapsed_Widget(w, iFalse);
687 }
688 if (isCommand_Widget(w, ev, "focus.lost")) {
689 setCursor_LookupWidget_(d, iInvalidPos);
690 }
691 if (isCommand_Widget(w, ev, "focus.gained")) {
692 if (d->cursor == iInvalidPos) {
693 setCursor_LookupWidget_(d, 1);
694 }
695 }
696 if (isCommand_Widget(w, ev, "list.clicked")) {
697 iInputWidget *url = findWidget_App("url");
698 const iLookupItem *item = constItem_ListWidget(d->list, arg_Command(cmd));
699 if (item && !isEmpty_String(&item->command)) {
700 setText_InputWidget(url, url_DocumentWidget(document_App()));
701 showCollapsed_Widget(w, iFalse);
702 setCursor_LookupWidget_(d, iInvalidPos);
703 postCommandString_Root(get_Root(), &item->command);
704 postCommand_App("focus.set id:"); /* unfocus */
705 }
706 return iTrue;
707 }
708 if (ev->type == SDL_MOUSEMOTION) {
709 if (contains_Widget(w, init_I2(ev->motion.x, ev->motion.y))) {
710 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_HAND);
711 }
712 return iFalse;
713 }
714 if (ev->type == SDL_KEYDOWN) {
715 const int mods = keyMods_Sym(ev->key.keysym.mod);
716 const int key = ev->key.keysym.sym;
717 if (isFocused_Widget(d)) {
718 iWidget *url = findWidget_App("url");
719 switch (key) {
720 case SDLK_ESCAPE:
721 showCollapsed_Widget(w, iFalse);
722 setCursor_LookupWidget_(d, iInvalidPos);
723 setFocus_Widget(url);
724 return iTrue;
725 case SDLK_UP:
726 if (!moveCursor_LookupWidget_(d, -1)) {
727 setCursor_LookupWidget_(d, iInvalidPos);
728 setFocus_Widget(url);
729 }
730 return iTrue;
731 case SDLK_DOWN:
732 moveCursor_LookupWidget_(d, +1);
733 return iTrue;
734 case SDLK_PAGEUP:
735 moveCursor_LookupWidget_(d, -visCount_ListWidget(d->list) + 1);
736 return iTrue;
737 case SDLK_PAGEDOWN:
738 moveCursor_LookupWidget_(d, visCount_ListWidget(d->list) - 1);
739 return iTrue;
740 case SDLK_HOME:
741 setCursor_LookupWidget_(d, 1);
742 return iTrue;
743 case SDLK_END:
744 setCursor_LookupWidget_(d, numItems_ListWidget(d->list) - 1);
745 return iTrue;
746 case SDLK_KP_ENTER:
747 case SDLK_SPACE:
748 case SDLK_RETURN:
749 postCommand_Widget(w, "list.clicked arg:%zu", d->cursor);
750 return iTrue;
751 }
752 }
753 /* Focus switching between URL bar and lookup results. */
754 if (isVisible_Widget(w)) {
755 if (((key == SDLK_DOWN && !mods) || key == SDLK_TAB) &&
756 focus_Widget() == findWidget_App("url") &&
757 numItems_ListWidget(d->list)) {
758 setCursor_LookupWidget_(d, 1); /* item 0 is always the first heading */
759 setFocus_Widget(w);
760 return iTrue;
761 }
762 else if (key == SDLK_TAB && isFocused_Widget(w)) {
763 setFocus_Widget(findWidget_App("url"));
764 return iTrue;
765 }
766 }
767 }
768 return processEvent_Widget(w, ev);
769 }
770
771 iBeginDefineSubclass(LookupWidget, Widget)
772 .draw = (iAny *) draw_LookupWidget_,
773 .processEvent = (iAny *) processEvent_LookupWidget_,
774 iEndDefineSubclass(LookupWidget)
775