summaryrefslogtreecommitdiff
path: root/src/ui/lookupwidget.c
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2020-09-06 22:45:15 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2020-09-06 22:45:15 +0300
commita364d9456dfdfd8181904fca6308e9c36eefd10a (patch)
treef355ded227cf52053784b991f5d8441a5502e447 /src/ui/lookupwidget.c
parent52a1652536e4e27751ac121009f85113e72afe7d (diff)
LookupWidget: Keyboard focus and cursor
Diffstat (limited to 'src/ui/lookupwidget.c')
-rw-r--r--src/ui/lookupwidget.c374
1 files changed, 362 insertions, 12 deletions
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index d2550c16..fbb5d365 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -23,19 +23,26 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
23#include "lookupwidget.h" 23#include "lookupwidget.h"
24#include "lookup.h" 24#include "lookup.h"
25#include "listwidget.h" 25#include "listwidget.h"
26#include "inputwidget.h"
27#include "util.h"
28#include "command.h"
29#include "bookmarks.h"
30#include "gmutil.h"
31#include "app.h"
26 32
27#include <the_Foundation/mutex.h> 33#include <the_Foundation/mutex.h>
28#include <the_Foundation/thread.h> 34#include <the_Foundation/thread.h>
35#include <the_Foundation/regexp.h>
29 36
30iDeclareType(LookupJob) 37iDeclareType(LookupJob)
31 38
32struct Impl_LookupJob { 39struct Impl_LookupJob {
33 iString term; 40 iRegExp *term;
34 iPtrArray results; 41 iPtrArray results;
35}; 42};
36 43
37static void init_LookupJob(iLookupJob *d) { 44static void init_LookupJob(iLookupJob *d) {
38 init_String(&d->term); 45 d->term = NULL;
39 init_PtrArray(&d->results); 46 init_PtrArray(&d->results);
40} 47}
41 48
@@ -44,23 +51,124 @@ static void deinit_LookupJob(iLookupJob *d) {
44 delete_LookupResult(i.ptr); 51 delete_LookupResult(i.ptr);
45 } 52 }
46 deinit_PtrArray(&d->results); 53 deinit_PtrArray(&d->results);
47 deinit_String(&d->term); 54 iRelease(d->term);
48} 55}
49 56
50iDefineTypeConstruction(LookupJob) 57iDefineTypeConstruction(LookupJob)
51 58
59/*----------------------------------------------------------------------------------------------*/
60
61iDeclareType(LookupItem)
62typedef iListItemClass iLookupItemClass;
63
64struct Impl_LookupItem {
65 iListItem listItem;
66 iLookupResult *result;
67 int font;
68 int fg;
69 iString text;
70 iString command;
71};
72
73static void init_LookupItem(iLookupItem *d, const iLookupResult *res) {
74 init_ListItem(&d->listItem);
75 d->result = res ? copy_LookupResult(res) : NULL;
76 d->font = uiContent_FontId;
77 d->fg = uiText_ColorId;
78 init_String(&d->text);
79 init_String(&d->command);
80}
81
82static void deinit_LookupItem(iLookupItem *d) {
83 deinit_String(&d->command);
84 deinit_String(&d->text);
85 delete_LookupResult(d->result);
86}
87
88static void draw_LookupItem_(iLookupItem *d, iPaint *p, iRect rect, const iListWidget *list) {
89 const iBool isPressing = isMouseDown_ListWidget(list);
90 const iBool isHover = isHover_Widget(list) && constHoverItem_ListWidget(list) == d;
91 const iBool isCursor = d->listItem.isSelected;
92 if (isHover || isCursor) {
93 fillRect_Paint(p,
94 rect,
95 isPressing || isCursor ? uiBackgroundPressed_ColorId
96 : uiBackgroundFramelessHover_ColorId);
97 }
98 int fg = isHover || isCursor
99 ? permanent_ColorId | (isPressing || isCursor ? uiTextPressed_ColorId
100 : uiTextFramelessHover_ColorId)
101 : d->fg;
102 const iInt2 size = measure_Text(d->font, cstr_String(&d->text));
103 iInt2 pos = init_I2(left_Rect(rect) + 3 * gap_UI, mid_Rect(rect).y - size.y / 2);
104 if (d->listItem.isSeparator) {
105 pos.y = bottom_Rect(rect) - lineHeight_Text(d->font) - gap_UI;
106 }
107 drawRange_Text(d->font, pos, fg, range_String(&d->text));
108}
109
110iBeginDefineSubclass(LookupItem, ListItem)
111 .draw = (iAny *) draw_LookupItem_,
112iEndDefineSubclass(LookupItem)
113
114iDefineObjectConstructionArgs(LookupItem, (const iLookupResult *res), res)
115
116/*----------------------------------------------------------------------------------------------*/
117
52struct Impl_LookupWidget { 118struct Impl_LookupWidget {
53 iWidget widget; 119 iWidget widget;
54 iListWidget *list; 120 iListWidget *list;
55 iThread *work; 121 size_t cursor;
56 iCondition jobAvailable; /* wakes up the work thread */ 122 iThread * work;
57 iMutex *mtx; 123 iCondition jobAvailable; /* wakes up the work thread */
58 iString nextJob; 124 iMutex * mtx;
59 iLookupJob *finishedJob; 125 iString nextJob;
126 iLookupJob * finishedJob;
60}; 127};
61 128
129static float scoreMatch_(const iRegExp *pattern, iRangecc text) {
130 float score = 0.0f;
131 iRegExpMatch m;
132 init_RegExpMatch(&m);
133 while (matchRange_RegExp(pattern, text, &m)) {
134 /* Match near the beginning is scored higher. */
135 score += (float) size_Range(&m.range) / ((float) m.range.start + 1);
136 }
137 return score;
138}
139
140static float bookmarkRelevance_LookupJob_(const iLookupJob *d, const iBookmark *bm) {
141 iUrl parts;
142 init_Url(&parts, &bm->url);
143 const float t = scoreMatch_(d->term, range_String(&bm->title));
144 const float h = scoreMatch_(d->term, parts.host);
145 const float p = scoreMatch_(d->term, parts.path);
146 const float g = scoreMatch_(d->term, range_String(&bm->tags));
147 return h + iMax(p, t) + 2 * g; /* extra weight for tags */
148}
149
150static iBool matchBookmark_LookupJob_(void *context, const iBookmark *bm) {
151 return bookmarkRelevance_LookupJob_(context, bm) > 0;
152}
153
154static void searchBookmarks_LookupJob_(iLookupJob *d) {
155 /* Note: Called in a background thread. */
156 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), NULL, matchBookmark_LookupJob_, d)) {
157 const iBookmark *bm = i.ptr;
158 iLookupResult * res = new_LookupResult();
159 res->type = bookmark_LookupResultType;
160 res->relevance = bookmarkRelevance_LookupJob_(d, bm);
161 set_String(&res->label, &bm->title);
162// appendFormat_String(&res->label, " (%f)", res->relevance);
163 set_String(&res->url, &bm->url);
164 res->when = bm->when;
165 pushBack_PtrArray(&d->results, res);
166 }
167}
168
62static iThreadResult worker_LookupWidget_(iThread *thread) { 169static iThreadResult worker_LookupWidget_(iThread *thread) {
63 iLookupWidget *d = userData_Thread(thread); 170 iLookupWidget *d = userData_Thread(thread);
171 printf("[LookupWidget] worker is running\n"); fflush(stdout);
64 lock_Mutex(d->mtx); 172 lock_Mutex(d->mtx);
65 for (;;) { 173 for (;;) {
66 wait_Condition(&d->jobAvailable, d->mtx); 174 wait_Condition(&d->jobAvailable, d->mtx);
@@ -68,11 +176,31 @@ static iThreadResult worker_LookupWidget_(iThread *thread) {
68 break; /* Time to quit. */ 176 break; /* Time to quit. */
69 } 177 }
70 iLookupJob *job = new_LookupJob(); 178 iLookupJob *job = new_LookupJob();
71 set_String(&job->term, &d->nextJob); 179 /* Make a regular expression to search for multiple alternative words. */ {
180 iString *pattern = new_String();
181 iRangecc word = iNullRange;
182 iBool isFirst = iTrue;
183 while (nextSplit_Rangecc(range_String(&d->nextJob), " ", &word)) {
184 if (isEmpty_Range(&word)) continue;
185 if (!isFirst) appendChar_String(pattern, '|');
186 for (const char *ch = word.start; ch != word.end; ch++) {
187 /* Escape regular expression characters. */
188 if (isSyntaxChar_RegExp(*ch)) {
189 appendChar_String(pattern, '\\');
190 }
191 appendChar_String(pattern, *ch);
192 }
193 isFirst = iFalse;
194 }
195 iAssert(!isEmpty_String(pattern));
196// printf("{%s}\n", cstr_String(pattern));
197 job->term = new_RegExp(cstr_String(pattern), caseInsensitive_RegExpOption);
198 delete_String(pattern);
199 }
72 clear_String(&d->nextJob); 200 clear_String(&d->nextJob);
73 unlock_Mutex(d->mtx); 201 unlock_Mutex(d->mtx);
74 /* Do the lookup. */ { 202 /* Do the lookup. */ {
75 203 searchBookmarks_LookupJob_(job);
76 } 204 }
77 /* Submit the result. */ 205 /* Submit the result. */
78 lock_Mutex(d->mtx); 206 lock_Mutex(d->mtx);
@@ -80,7 +208,10 @@ static iThreadResult worker_LookupWidget_(iThread *thread) {
80 /* Previous results haven't been taken yet. */ 208 /* Previous results haven't been taken yet. */
81 delete_LookupJob(d->finishedJob); 209 delete_LookupJob(d->finishedJob);
82 } 210 }
211 printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results));
212 fflush(stdout);
83 d->finishedJob = job; 213 d->finishedJob = job;
214 postCommand_Widget(as_Widget(d), "lookup.ready");
84 } 215 }
85 unlock_Mutex(d->mtx); 216 unlock_Mutex(d->mtx);
86 printf("[LookupWidget] worker has quit\n"); fflush(stdout); 217 printf("[LookupWidget] worker has quit\n"); fflush(stdout);
@@ -93,14 +224,17 @@ void init_LookupWidget(iLookupWidget *d) {
93 iWidget *w = as_Widget(d); 224 iWidget *w = as_Widget(d);
94 init_Widget(w); 225 init_Widget(w);
95 setId_Widget(w, "lookup"); 226 setId_Widget(w, "lookup");
96 setFlags_Widget(w, resizeChildren_WidgetFlag, iTrue); 227 setFlags_Widget(w, focusable_WidgetFlag | resizeChildren_WidgetFlag, iTrue);
97 d->list = addChild_Widget(w, iClob(new_ListWidget())); 228 d->list = addChild_Widget(w, iClob(new_ListWidget()));
229 setItemHeight_ListWidget(d->list, lineHeight_Text(default_FontId) * 2);
230 d->cursor = iInvalidPos;
98 d->work = new_Thread(worker_LookupWidget_); 231 d->work = new_Thread(worker_LookupWidget_);
99 setUserData_Thread(d->work, d); 232 setUserData_Thread(d->work, d);
100 init_Condition(&d->jobAvailable); 233 init_Condition(&d->jobAvailable);
101 d->mtx = new_Mutex(); 234 d->mtx = new_Mutex();
102 init_String(&d->nextJob); 235 init_String(&d->nextJob);
103 d->finishedJob = NULL; 236 d->finishedJob = NULL;
237 start_Thread(d->work);
104} 238}
105 239
106void deinit_LookupWidget(iLookupWidget *d) { 240void deinit_LookupWidget(iLookupWidget *d) {
@@ -118,13 +252,229 @@ void deinit_LookupWidget(iLookupWidget *d) {
118 deinit_Condition(&d->jobAvailable); 252 deinit_Condition(&d->jobAvailable);
119} 253}
120 254
255void submit_LookupWidget(iLookupWidget *d, const iString *term) {
256 iGuardMutex(d->mtx, {
257 set_String(&d->nextJob, term);
258 trim_String(&d->nextJob);
259 if (!isEmpty_String(&d->nextJob)) {
260 signal_Condition(&d->jobAvailable);
261 }
262 else {
263 setFlags_Widget(as_Widget(d), hidden_WidgetFlag, iTrue);
264 }
265 });
266}
267
121static void draw_LookupWidget_(const iLookupWidget *d) { 268static void draw_LookupWidget_(const iLookupWidget *d) {
122 const iWidget *w = constAs_Widget(d); 269 const iWidget *w = constAs_Widget(d);
123 draw_Widget(w); 270 draw_Widget(w);
271 /* Draw a frame. */ {
272 iPaint p;
273 init_Paint(&p);
274 drawRect_Paint(&p,
275 bounds_Widget(w),
276 isFocused_Widget(w) ? uiInputFrameFocused_ColorId : uiSeparator_ColorId);
277 }
278}
279
280static int cmpPtr_LookupResult_(const void *p1, const void *p2) {
281 const iLookupResult *a = *(const iLookupResult **) p1;
282 const iLookupResult *b = *(const iLookupResult **) p2;
283 if (a->type != b->type) {
284 return iCmp(a->type, b->type);
285 }
286 if (fabsf(a->relevance - b->relevance) < 0.0001f) {
287 return cmpString_String(&a->url, &b->url);
288 }
289 return -iCmp(a->relevance, b->relevance);
290}
291
292static const char *cstr_LookupResultType(enum iLookupResultType d) {
293 switch (d) {
294 case bookmark_LookupResultType:
295 return "BOOKMARKS";
296 case history_LookupResultType:
297 return "HISTORY";
298 case content_LookupResultType:
299 return "PAGE CONTENTS";
300 case identity_LookupResultType:
301 return "IDENTITIES";
302 default:
303 return "OTHER";
304 }
305}
306
307static void presentResults_LookupWidget_(iLookupWidget *d) {
308 iLookupJob *job;
309 iGuardMutex(d->mtx, {
310 job = d->finishedJob;
311 d->finishedJob = NULL;
312 });
313 if (!job) return;
314 clear_ListWidget(d->list);
315 sort_Array(&job->results, cmpPtr_LookupResult_);
316 enum iLookupResultType lastType = none_LookupResultType;
317 iConstForEach(PtrArray, i, &job->results) {
318 const iLookupResult *res = i.ptr;
319 if (lastType != res->type) {
320 /* Heading separator. */
321 iLookupItem *item = new_LookupItem(NULL);
322 item->listItem.isSeparator = iTrue;
323 item->fg = uiHeading_ColorId;
324 item->font = default_FontId;
325 format_String(&item->text, "%s", cstr_LookupResultType(res->type));
326 addItem_ListWidget(d->list, item);
327 iRelease(item);
328 lastType = res->type;
329 }
330 iLookupItem *item = new_LookupItem(res);
331 switch (res->type) {
332 case bookmark_LookupResultType: {
333 item->fg = uiTextStrong_ColorId;
334 item->font = default_FontId;
335 const char *url = cstr_String(&res->url);
336 if (startsWithCase_String(&res->url, "gemini://")) {
337 url += 9;
338 }
339 format_String(&item->text, "%s\n%s Open %s", cstr_String(&res->label),
340 uiText_ColorEscape, url);
341 format_String(&item->command, "open url:%s", cstr_String(&res->url));
342 break;
343 }
344 }
345 addItem_ListWidget(d->list, item);
346 iRelease(item);
347 }
348 delete_LookupJob(job);
349 /* Re-select the item at the cursor. */
350 if (d->cursor != iInvalidPos) {
351 d->cursor = iMin(d->cursor, numItems_ListWidget(d->list) - 1);
352 ((iListItem *) item_ListWidget(d->list, d->cursor))->isSelected = iTrue;
353 }
354 updateVisible_ListWidget(d->list);
355 invalidate_ListWidget(d->list);
356 setFlags_Widget(as_Widget(d), hidden_WidgetFlag, numItems_ListWidget(d->list) == 0);
357}
358
359static iLookupItem *item_LookupWidget_(iLookupWidget *d, size_t index) {
360 return item_ListWidget(d->list, index);
361}
362
363static void setCursor_LookupWidget_(iLookupWidget *d, size_t index) {
364 if (index != d->cursor) {
365 iLookupItem *item = item_LookupWidget_(d, d->cursor);
366 if (item) {
367 item->listItem.isSelected = iFalse;
368 invalidateItem_ListWidget(d->list, d->cursor);
369 }
370 d->cursor = index;
371 if ((item = item_LookupWidget_(d, d->cursor)) != NULL) {
372 item->listItem.isSelected = iTrue;
373 invalidateItem_ListWidget(d->list, d->cursor);
374 }
375 }
124} 376}
125 377
126static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) { 378static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {
127 iWidget *w = as_Widget(d); 379 iWidget *w = as_Widget(d);
380 const char *cmd = command_UserEvent(ev);
381// if (ev->type == SDL_MOUSEMOTION && contains_Widget(w, init_I2(ev->motion.x, ev->motion.y))) {
382// setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
383// }
384 if (isCommand_Widget(w, ev, "lookup.ready")) {
385 /* Take the results and present them in the list. */
386 presentResults_LookupWidget_(d);
387 return iTrue;
388 }
389 if (isResize_UserEvent(ev)) {
390 /* Position the lookup popup under the URL bar. */ {
391 setSize_Widget(w, init_I2(width_Widget(findWidget_App("url")),
392 get_Window()->root->rect.size.y / 2));
393 setPos_Widget(w, bottomLeft_Rect(bounds_Widget(findWidget_App("url"))));
394 arrange_Widget(w);
395 }
396 updateVisible_ListWidget(d->list);
397 invalidate_ListWidget(d->list);
398 }
399 if (equal_Command(cmd, "input.ended") && !cmp_String(string_Command(cmd, "id"), "url") &&
400 !isFocused_Widget(w)) {
401 setFlags_Widget(w, hidden_WidgetFlag, iTrue);
402 }
403 if (isCommand_Widget(w, ev, "focus.lost")) {
404 setCursor_LookupWidget_(d, iInvalidPos);
405 }
406 if (isCommand_Widget(w, ev, "focus.gained")) {
407 if (d->cursor == iInvalidPos) {
408 setCursor_LookupWidget_(d, 1);
409 }
410 }
411 if (isCommand_Widget(w, ev, "list.clicked")) {
412 setTextCStr_InputWidget(findWidget_App("url"), "");
413 const iLookupItem *item = constItem_ListWidget(d->list, arg_Command(cmd));
414 if (item && !isEmpty_String(&item->command)) {
415 postCommandString_App(&item->command);
416 setFlags_Widget(w, hidden_WidgetFlag, iTrue);
417 setCursor_LookupWidget_(d, iInvalidPos);
418 setFocus_Widget(NULL);
419 }
420 return iTrue;
421 }
422 if (ev->type == SDL_KEYDOWN) {
423 const int mods = keyMods_Sym(ev->key.keysym.mod);
424 const int key = ev->key.keysym.sym;
425 if (isFocused_Widget(d)) {
426 iWidget *url = findWidget_App("url");
427 switch (key) {
428 case SDLK_ESCAPE:
429 setFlags_Widget(w, hidden_WidgetFlag, iTrue);
430 setCursor_LookupWidget_(d, iInvalidPos);
431 setFocus_Widget(url);
432 return iTrue;
433 case SDLK_UP:
434 for (;;) {
435 if (d->cursor == 0) {
436 setCursor_LookupWidget_(d, iInvalidPos);
437 setFocus_Widget(url);
438 break;
439 }
440 setCursor_LookupWidget_(d, d->cursor - 1);
441 if (!item_LookupWidget_(d, d->cursor)->listItem.isSeparator) {
442 break;
443 }
444 }
445 return iTrue;
446 case SDLK_DOWN:
447 while (d->cursor < numItems_ListWidget(d->list) - 1) {
448 setCursor_LookupWidget_(d, d->cursor + 1);
449 if (!item_LookupWidget_(d, d->cursor)->listItem.isSeparator) {
450 break;
451 }
452 }
453 return iTrue;
454 case SDLK_PAGEUP:
455 return iTrue;
456 case SDLK_PAGEDOWN:
457 return iTrue;
458 case SDLK_HOME:
459 setCursor_LookupWidget_(d, 1);
460 return iTrue;
461 case SDLK_END:
462 setCursor_LookupWidget_(d, numItems_ListWidget(d->list) - 1);
463 return iTrue;
464 case SDLK_KP_ENTER:
465 case SDLK_SPACE:
466 case SDLK_RETURN:
467 postCommand_Widget(w, "list.clicked arg:%zu", d->cursor);
468 return iTrue;
469 }
470 }
471 if (key == SDLK_DOWN && !mods && focus_Widget() == findWidget_App("url") &&
472 numItems_ListWidget(d->list)) {
473 setCursor_LookupWidget_(d, 1); /* item 0 is always the first heading */
474 setFocus_Widget(w);
475 return iTrue;
476 }
477 }
128 return processEvent_Widget(w, ev); 478 return processEvent_Widget(w, ev);
129} 479}
130 480