summaryrefslogtreecommitdiff
path: root/src/ui/certlistwidget.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/certlistwidget.c')
-rw-r--r--src/ui/certlistwidget.c490
1 files changed, 490 insertions, 0 deletions
diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c
new file mode 100644
index 00000000..2a7562d8
--- /dev/null
+++ b/src/ui/certlistwidget.c
@@ -0,0 +1,490 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. 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
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY 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
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "certlistwidget.h"
24
25#include "documentwidget.h"
26#include "command.h"
27#include "labelwidget.h"
28#include "listwidget.h"
29#include "../gmcerts.h"
30#include "../app.h"
31
32#include <SDL_clipboard.h>
33
34iDeclareType(CertItem)
35typedef iListItemClass iCertItemClass;
36
37struct Impl_CertItem {
38 iListItem listItem;
39 uint32_t id;
40 int indent;
41 iChar icon;
42 iBool isBold;
43 iString label;
44 iString meta;
45// iString url;
46};
47
48void init_CertItem(iCertItem *d) {
49 init_ListItem(&d->listItem);
50 d->id = 0;
51 d->indent = 0;
52 d->icon = 0;
53 d->isBold = iFalse;
54 init_String(&d->label);
55 init_String(&d->meta);
56// init_String(&d->url);
57}
58
59void deinit_CertItem(iCertItem *d) {
60// deinit_String(&d->url);
61 deinit_String(&d->meta);
62 deinit_String(&d->label);
63}
64
65static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect, const iListWidget *list);
66
67iBeginDefineSubclass(CertItem, ListItem)
68 .draw = (iAny *) draw_CertItem_,
69iEndDefineSubclass(CertItem)
70
71iDefineObjectConstruction(CertItem)
72
73/*----------------------------------------------------------------------------------------------*/
74
75struct Impl_CertListWidget {
76 iListWidget list;
77 int itemFonts[2];
78 iWidget *menu; /* context menu for an item */
79 iCertItem *contextItem; /* list item accessed in the context menu */
80 size_t contextIndex; /* index of list item accessed in the context menu */
81};
82
83iDefineObjectConstruction(CertListWidget)
84
85static iGmIdentity *menuIdentity_CertListWidget_(const iCertListWidget *d) {
86 if (d->contextItem) {
87 return identity_GmCerts(certs_App(), d->contextItem->id);
88 }
89 return NULL;
90}
91
92static void updateContextMenu_CertListWidget_(iCertListWidget *d) {
93 iArray *items = collectNew_Array(sizeof(iMenuItem));
94 const iString *docUrl = url_DocumentWidget(document_App());
95 size_t firstIndex = 0;
96 if (deviceType_App() != desktop_AppDeviceType && !isEmpty_String(docUrl)) {
97 pushBack_Array(items, &(iMenuItem){ format_CStr("```%s", cstr_String(docUrl)) });
98 firstIndex = 1;
99 }
100 const iMenuItem ctxItems[] = {
101 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
102 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
103 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
104 { "---", 0, 0, NULL },
105 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
106 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
107#if defined (iPlatformAppleDesktop)
108 { magnifyingGlass_Icon " ${menu.reveal.macos}", 0, 0, "ident.reveal" },
109#endif
110#if defined (iPlatformLinux)
111 { magnifyingGlass_Icon " ${menu.reveal.filemgr}", 0, 0, "ident.reveal" },
112#endif
113 { export_Icon " ${ident.export}", 0, 0, "ident.export" },
114 { "---", 0, 0, NULL },
115 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
116 };
117 pushBackN_Array(items, ctxItems, iElemCount(ctxItems));
118 /* Used URLs. */
119 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
120 if (ident) {
121 size_t insertPos = firstIndex + 3;
122 if (!isEmpty_StringSet(ident->useUrls)) {
123 insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL });
124 }
125 iBool usedOnCurrentPage = iFalse;
126 iConstForEach(StringSet, i, ident->useUrls) {
127 const iString *url = i.value;
128 usedOnCurrentPage |= startsWithCase_String(docUrl, cstr_String(url));
129 iRangecc urlStr = range_String(url);
130 if (startsWith_Rangecc(urlStr, "gemini://")) {
131 urlStr.start += 9; /* omit the default scheme */
132 }
133 insert_Array(items,
134 insertPos++,
135 &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)),
136 0,
137 0,
138 format_CStr("!open url:%s", cstr_String(url)) });
139 }
140 if (!usedOnCurrentPage) {
141 remove_Array(items, firstIndex + 1);
142 }
143 else {
144 remove_Array(items, firstIndex);
145 }
146 }
147 destroy_Widget(d->menu);
148 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
149}
150
151static void itemClicked_CertListWidget_(iCertListWidget *d, iCertItem *item, size_t itemIndex) {
152 iWidget *w = as_Widget(d);
153 setFocus_Widget(NULL);
154 d->contextItem = item;
155 if (d->contextIndex != iInvalidPos) {
156 invalidateItem_ListWidget(&d->list, d->contextIndex);
157 }
158 d->contextIndex = itemIndex;
159 if (itemIndex < numItems_ListWidget(&d->list)) {
160 updateContextMenu_CertListWidget_(d);
161 arrange_Widget(d->menu);
162 openMenu_Widget(d->menu,
163 bounds_Widget(w).pos.x < mid_Rect(rect_Root(w->root)).x
164 ? topRight_Rect(itemRect_ListWidget(&d->list, itemIndex))
165 : addX_I2(topLeft_Rect(itemRect_ListWidget(&d->list, itemIndex)),
166 -width_Widget(d->menu)));
167 }
168}
169
170static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *ev) {
171 iWidget *w = as_Widget(d);
172 /* Handle commands. */
173 if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) {
174 const char *cmd = command_UserEvent(ev);
175 if (equal_Command(cmd, "idents.changed")) {
176 updateItems_CertListWidget(d);
177 }
178 else if (isCommand_Widget(w, ev, "list.clicked")) {
179 itemClicked_CertListWidget_(
180 d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg"));
181 return iTrue;
182 }
183 else if (isCommand_Widget(w, ev, "ident.use")) {
184 iGmIdentity *ident = menuIdentity_CertListWidget_(d);
185 const iString *tabUrl = urlQueryStripped_String(url_DocumentWidget(document_App()));
186 if (ident) {
187 if (argLabel_Command(cmd, "clear")) {
188 clearUse_GmIdentity(ident);
189 }
190 else if (arg_Command(cmd)) {
191 signIn_GmCerts(certs_App(), ident, tabUrl);
192 postCommand_App("navigate.reload");
193 }
194 else {
195 signOut_GmCerts(certs_App(), tabUrl);
196 postCommand_App("navigate.reload");
197 }
198 saveIdentities_GmCerts(certs_App());
199 updateItems_CertListWidget(d);
200 }
201 return iTrue;
202 }
203 else if (isCommand_Widget(w, ev, "ident.edit")) {
204 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
205 if (ident) {
206 makeValueInput_Widget(get_Root()->widget,
207 &ident->notes,
208 uiHeading_ColorEscape "${heading.ident.notes}",
209 format_CStr(cstr_Lang("dlg.ident.notes"), cstr_String(name_GmIdentity(ident))),
210 uiTextAction_ColorEscape "${dlg.default}",
211 format_CStr("!ident.setnotes ident:%p ptr:%p", ident, d));
212 }
213 return iTrue;
214 }
215 else if (isCommand_Widget(w, ev, "ident.fingerprint")) {
216 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
217 if (ident) {
218 const iString *fps = collect_String(
219 hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert))));
220 SDL_SetClipboardText(cstr_String(fps));
221 }
222 return iTrue;
223 }
224 else if (isCommand_Widget(w, ev, "ident.export")) {
225 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
226 if (ident) {
227 iString *pem = collect_String(pem_TlsCertificate(ident->cert));
228 append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert)));
229 iDocumentWidget *expTab = newTab_App(NULL, iTrue);
230 setUrlAndSource_DocumentWidget(
231 expTab,
232 collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))),
233 collectNewCStr_String("text/plain"),
234 utf8_String(pem));
235 }
236 return iTrue;
237 }
238 else if (isCommand_Widget(w, ev, "ident.setnotes")) {
239 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
240 if (ident) {
241 setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value"));
242 updateItems_CertListWidget(d);
243 }
244 return iTrue;
245 }
246 else if (isCommand_Widget(w, ev, "ident.pickicon")) {
247 return iTrue;
248 }
249 else if (isCommand_Widget(w, ev, "ident.reveal")) {
250 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
251 if (ident) {
252 const iString *crtPath = certificatePath_GmCerts(certs_App(), ident);
253 if (crtPath) {
254 postCommandf_App("reveal path:%s", cstr_String(crtPath));
255 }
256 }
257 return iTrue;
258 }
259 else if (isCommand_Widget(w, ev, "ident.delete")) {
260 iCertItem *item = d->contextItem;
261 if (argLabel_Command(cmd, "confirm")) {
262 makeQuestion_Widget(
263 uiTextCaution_ColorEscape "${heading.ident.delete}",
264 format_CStr(cstr_Lang("dlg.confirm.ident.delete"),
265 uiTextAction_ColorEscape,
266 cstr_String(&item->label),
267 uiText_ColorEscape),
268 (iMenuItem[]){ { "${cancel}", 0, 0, NULL },
269 { uiTextCaution_ColorEscape "${dlg.ident.delete}",
270 0,
271 0,
272 format_CStr("!ident.delete confirm:0 ptr:%p", d) } },
273 2);
274 return iTrue;
275 }
276 deleteIdentity_GmCerts(certs_App(), menuIdentity_CertListWidget_(d));
277 postCommand_App("idents.changed");
278 return iTrue;
279 }
280 }
281 if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) {
282 const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y);
283 /* Update cursor. */
284 if (contains_Widget(w, mouse)) {
285 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
286 }
287 else if (d->contextIndex != iInvalidPos) {
288 invalidateItem_ListWidget(&d->list, d->contextIndex);
289 d->contextIndex = iInvalidPos;
290 }
291 }
292 /* Update context menu items. */
293 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) {
294 d->contextItem = NULL;
295 if (!isVisible_Widget(d->menu)) {
296 updateMouseHover_ListWidget(&d->list);
297 }
298 if (constHoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) {
299 d->contextItem = hoverItem_ListWidget(&d->list);
300 /* Context is drawn in hover state. */
301 if (d->contextIndex != iInvalidPos) {
302 invalidateItem_ListWidget(&d->list, d->contextIndex);
303 }
304 d->contextIndex = hoverItemIndex_ListWidget(&d->list);
305 updateContextMenu_CertListWidget_(d);
306 /* TODO: Some callback-based mechanism would be nice for updating menus right
307 before they open? At least move these to `updateContextMenu_ */
308 const iGmIdentity *ident = constHoverIdentity_CertListWidget(d);
309 const iString * docUrl = url_DocumentWidget(document_App());
310 iForEach(ObjectList, i, children_Widget(d->menu)) {
311 if (isInstance_Object(i.object, &Class_LabelWidget)) {
312 iLabelWidget *menuItem = i.object;
313 const char * cmdItem = cstr_String(command_LabelWidget(menuItem));
314 if (equal_Command(cmdItem, "ident.use")) {
315 const iBool cmdUse = arg_Command(cmdItem) != 0;
316 const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0;
317 setFlags_Widget(
318 as_Widget(menuItem),
319 disabled_WidgetFlag,
320 (cmdClear && !isUsed_GmIdentity(ident)) ||
321 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) ||
322 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl)));
323 }
324 }
325 }
326 }
327 if (hoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) {
328 processContextMenuEvent_Widget(d->menu, ev, {});
329 }
330 }
331 return ((iWidgetClass *) class_Widget(w)->super)->processEvent(w, ev);
332}
333
334static void draw_CertListWidget_(const iCertListWidget *d) {
335 const iWidget *w = constAs_Widget(d);
336 ((iWidgetClass *) class_Widget(w)->super)->draw(w);
337}
338
339static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect,
340 const iListWidget *list) {
341 const iCertListWidget *certList = (const iCertListWidget *) list;
342 const iBool isMenuVisible = isVisible_Widget(certList->menu);
343 const iBool isDragging = constDragItem_ListWidget(list) == d;
344 const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging;
345 const iBool isHover =
346 (!isMenuVisible &&
347 isHover_Widget(constAs_Widget(list)) &&
348 constHoverItem_ListWidget(list) == d) ||
349 (isMenuVisible && certList->contextItem == d) ||
350 isDragging;
351 const int itemHeight = height_Rect(itemRect);
352 const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId)
353 : uiIcon_ColorId;
354 const int altIconColor = isPressing ? uiTextPressed_ColorId : uiTextCaution_ColorId;
355 const int font = certList->itemFonts[d->isBold ? 1 : 0];
356 int bg = uiBackgroundSidebar_ColorId;
357 if (isHover) {
358 bg = isPressing ? uiBackgroundPressed_ColorId
359 : uiBackgroundFramelessHover_ColorId;
360 fillRect_Paint(p, itemRect, bg);
361 }
362 else if (d->listItem.isSelected) {
363 bg = uiBackgroundUnfocusedSelection_ColorId;
364 fillRect_Paint(p, itemRect, bg);
365 }
366// iInt2 pos = itemRect.pos;
367 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
368 : uiTextStrong_ColorId;
369 const iBool isUsedOnDomain = (d->indent != 0);
370 iString icon;
371 initUnicodeN_String(&icon, &d->icon, 1);
372 iInt2 cPos = topLeft_Rect(itemRect);
373 const int indent = 1.4f * lineHeight_Text(font);
374 addv_I2(&cPos,
375 init_I2(3 * gap_UI,
376 (itemHeight - lineHeight_Text(uiLabel_FontId) * 2 - lineHeight_Text(font)) /
377 2));
378 const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId
379 : uiTextFramelessHover_ColorId)
380 : uiTextDim_ColorId;
381 if (!d->listItem.isSelected && !isUsedOnDomain) {
382 drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon));
383 }
384 drawRange_Text(font,
385 cPos,
386 d->listItem.isSelected ? iconColor
387 : isUsedOnDomain ? altIconColor
388 : uiBackgroundSidebar_ColorId,
389 range_String(&icon));
390 deinit_String(&icon);
391 drawRange_Text(d->listItem.isSelected ? certList->itemFonts[1] : font,
392 add_I2(cPos, init_I2(indent, 0)),
393 fg,
394 range_String(&d->label));
395 drawRange_Text(uiLabel_FontId,
396 add_I2(cPos, init_I2(indent, lineHeight_Text(font))),
397 metaFg,
398 range_String(&d->meta));
399}
400
401void init_CertListWidget(iCertListWidget *d) {
402 iWidget *w = as_Widget(d);
403 init_ListWidget(&d->list);
404 setId_Widget(w, "certlist");
405 setBackgroundColor_Widget(w, none_ColorId);
406 d->itemFonts[0] = uiContent_FontId;
407 d->itemFonts[1] = uiContentBold_FontId;
408#if defined (iPlatformMobile)
409 if (deviceType_App() == phone_AppDeviceType) {
410 d->itemFonts[0] = uiLabelBig_FontId;
411 d->itemFonts[1] = uiLabelBigBold_FontId;
412 }
413#endif
414 updateItemHeight_CertListWidget(d);
415 d->menu = NULL;
416 d->contextItem = NULL;
417 d->contextIndex = iInvalidPos;
418}
419
420void updateItemHeight_CertListWidget(iCertListWidget *d) {
421 setItemHeight_ListWidget(&d->list, 3.5f * lineHeight_Text(d->itemFonts[0]));
422}
423
424iBool updateItems_CertListWidget(iCertListWidget *d) {
425 clear_ListWidget(&d->list);
426 destroy_Widget(d->menu);
427 d->menu = NULL;
428 const iString *tabUrl = url_DocumentWidget(document_App());
429 const iRangecc tabHost = urlHost_String(tabUrl);
430 iBool haveItems = iFalse;
431 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
432 const iGmIdentity *ident = i.ptr;
433 iCertItem *item = new_CertItem();
434 item->id = (uint32_t) index_PtrArrayConstIterator(&i);
435 item->icon = 0x1f464; /* person */
436 set_String(&item->label, name_GmIdentity(ident));
437 iDate until;
438 validUntil_TlsCertificate(ident->cert, &until);
439 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl);
440 format_String(&item->meta,
441 "%s",
442 isActive ? cstr_Lang("ident.using")
443 : isUsed_GmIdentity(ident)
444 ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls))
445 : cstr_Lang("ident.notused"));
446 const char *expiry =
447 ident->flags & temporary_GmIdentityFlag
448 ? cstr_Lang("ident.temporary")
449 : cstrCollect_String(format_Date(&until, cstr_Lang("ident.expiry")));
450 if (isEmpty_String(&ident->notes)) {
451 appendFormat_String(&item->meta, "\n%s", expiry);
452 }
453 else {
454 appendFormat_String(&item->meta,
455 " \u2014 %s\n%s%s",
456 expiry,
457 escape_Color(uiHeading_ColorId),
458 cstr_String(&ident->notes));
459 }
460 item->listItem.isSelected = isActive;
461 if (!isActive && isUsedOnDomain_GmIdentity(ident, tabHost)) {
462 item->indent = 1; /* will be highlighted */
463 }
464 addItem_ListWidget(&d->list, item);
465 haveItems = iTrue;
466 iRelease(item);
467 }
468 return haveItems;
469}
470
471void deinit_CertListWidget(iCertListWidget *d) {
472 iUnused(d);
473}
474
475const iGmIdentity *constHoverIdentity_CertListWidget(const iCertListWidget *d) {
476 const iCertItem *hoverItem = constHoverItem_ListWidget(&d->list);
477 if (hoverItem) {
478 return identity_GmCerts(certs_App(), hoverItem->id);
479 }
480 return NULL;
481}
482
483iGmIdentity *hoverIdentity_CertListWidget(const iCertListWidget *d) {
484 return iConstCast(iGmIdentity *, constHoverIdentity_CertListWidget(d));
485}
486
487iBeginDefineSubclass(CertListWidget, ListWidget)
488 .processEvent = (iAny *) processEvent_CertListWidget_,
489 .draw = (iAny *) draw_CertListWidget_,
490iEndDefineSubclass(CertListWidget)