summaryrefslogtreecommitdiff
path: root/src/ui/certlistwidget.c
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-12-02 10:02:16 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-12-02 10:02:16 +0200
commitb9cdb34c59dc133b549deed5a4f3b9bb95197cca (patch)
tree1ed42fabfff17fe5d7d2c0ed4c8687f471df345d /src/ui/certlistwidget.c
parentf4942e1b4da6dc1334dcdb4f2daae670bfa1f813 (diff)
Refactored CertListWidget out of the sidebar
The identity list is needed elsewhere outside of the sidebar, so moved it into a specialized ListWidget class.
Diffstat (limited to 'src/ui/certlistwidget.c')
-rw-r--r--src/ui/certlistwidget.c474
1 files changed, 474 insertions, 0 deletions
diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c
new file mode 100644
index 00000000..4d939ae2
--- /dev/null
+++ b/src/ui/certlistwidget.c
@@ -0,0 +1,474 @@
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 pushBackN_Array(items, (iMenuItem[]){
95 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
96 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
97 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
98 { "---", 0, 0, NULL },
99 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
100 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
101 { export_Icon " ${ident.export}", 0, 0, "ident.export" },
102 { "---", 0, 0, NULL },
103 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
104 }, 9);
105 /* Used URLs. */
106 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
107 if (ident) {
108 size_t insertPos = 3;
109 if (!isEmpty_StringSet(ident->useUrls)) {
110 insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL });
111 }
112 const iString *docUrl = url_DocumentWidget(document_App());
113 iBool usedOnCurrentPage = iFalse;
114 iConstForEach(StringSet, i, ident->useUrls) {
115 const iString *url = i.value;
116 usedOnCurrentPage |= equalCase_String(docUrl, url);
117 iRangecc urlStr = range_String(url);
118 if (startsWith_Rangecc(urlStr, "gemini://")) {
119 urlStr.start += 9; /* omit the default scheme */
120 }
121 insert_Array(items,
122 insertPos++,
123 &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)),
124 0,
125 0,
126 format_CStr("!open url:%s", cstr_String(url)) });
127 }
128 if (!usedOnCurrentPage) {
129 remove_Array(items, 1);
130 }
131 }
132 destroy_Widget(d->menu);
133 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
134}
135
136static void itemClicked_CertListWidget_(iCertListWidget *d, iCertItem *item, size_t itemIndex) {
137 iWidget *w = as_Widget(d);
138 setFocus_Widget(NULL);
139 d->contextItem = item;
140 if (d->contextIndex != iInvalidPos) {
141 invalidateItem_ListWidget(&d->list, d->contextIndex);
142 }
143 d->contextIndex = itemIndex;
144 if (itemIndex < numItems_ListWidget(&d->list)) {
145 updateContextMenu_CertListWidget_(d);
146 arrange_Widget(d->menu);
147 openMenu_Widget(d->menu,
148 bounds_Widget(w).pos.x < mid_Rect(rect_Root(w->root)).x
149 ? topRight_Rect(itemRect_ListWidget(&d->list, itemIndex))
150 : addX_I2(topLeft_Rect(itemRect_ListWidget(&d->list, itemIndex)),
151 -width_Widget(d->menu)));
152 }
153}
154
155static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *ev) {
156 iWidget *w = as_Widget(d);
157 /* Handle commands. */
158 if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) {
159 const char *cmd = command_UserEvent(ev);
160 if (equal_Command(cmd, "idents.changed")) {
161 updateItems_CertListWidget(d);
162 }
163 else if (isCommand_Widget(w, ev, "list.clicked")) {
164 itemClicked_CertListWidget_(
165 d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg"));
166 return iTrue;
167 }
168 else if (isCommand_Widget(w, ev, "ident.use")) {
169 iGmIdentity * ident = menuIdentity_CertListWidget_(d);
170 const iString *tabUrl = url_DocumentWidget(document_App());
171 if (ident) {
172 if (argLabel_Command(cmd, "clear")) {
173 clearUse_GmIdentity(ident);
174 }
175 else if (arg_Command(cmd)) {
176 signIn_GmCerts(certs_App(), ident, tabUrl);
177 postCommand_App("navigate.reload");
178 }
179 else {
180 signOut_GmCerts(certs_App(), tabUrl);
181 postCommand_App("navigate.reload");
182 }
183 saveIdentities_GmCerts(certs_App());
184 updateItems_CertListWidget(d);
185 }
186 return iTrue;
187 }
188 else if (isCommand_Widget(w, ev, "ident.edit")) {
189 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
190 if (ident) {
191 makeValueInput_Widget(get_Root()->widget,
192 &ident->notes,
193 uiHeading_ColorEscape "${heading.ident.notes}",
194 format_CStr(cstr_Lang("dlg.ident.notes"), cstr_String(name_GmIdentity(ident))),
195 uiTextAction_ColorEscape "${dlg.default}",
196 format_CStr("!ident.setnotes ident:%p ptr:%p", ident, d));
197 }
198 return iTrue;
199 }
200 else if (isCommand_Widget(w, ev, "ident.fingerprint")) {
201 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
202 if (ident) {
203 const iString *fps = collect_String(
204 hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert))));
205 SDL_SetClipboardText(cstr_String(fps));
206 }
207 return iTrue;
208 }
209 else if (isCommand_Widget(w, ev, "ident.export")) {
210 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
211 if (ident) {
212 iString *pem = collect_String(pem_TlsCertificate(ident->cert));
213 append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert)));
214 iDocumentWidget *expTab = newTab_App(NULL, iTrue);
215 setUrlAndSource_DocumentWidget(
216 expTab,
217 collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))),
218 collectNewCStr_String("text/plain"),
219 utf8_String(pem));
220 }
221 return iTrue;
222 }
223 else if (isCommand_Widget(w, ev, "ident.setnotes")) {
224 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
225 if (ident) {
226 setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value"));
227 updateItems_CertListWidget(d);
228 }
229 return iTrue;
230 }
231 else if (isCommand_Widget(w, ev, "ident.pickicon")) {
232 return iTrue;
233 }
234 else if (isCommand_Widget(w, ev, "ident.reveal")) {
235 const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
236 if (ident) {
237 const iString *crtPath = certificatePath_GmCerts(certs_App(), ident);
238 if (crtPath) {
239 revealPath_App(crtPath);
240 }
241 }
242 return iTrue;
243 }
244 else if (isCommand_Widget(w, ev, "ident.delete")) {
245 iCertItem *item = d->contextItem;
246 if (argLabel_Command(cmd, "confirm")) {
247 makeQuestion_Widget(
248 uiTextCaution_ColorEscape "${heading.ident.delete}",
249 format_CStr(cstr_Lang("dlg.confirm.ident.delete"),
250 uiTextAction_ColorEscape,
251 cstr_String(&item->label),
252 uiText_ColorEscape),
253 (iMenuItem[]){ { "${cancel}", 0, 0, NULL },
254 { uiTextCaution_ColorEscape "${dlg.ident.delete}",
255 0,
256 0,
257 format_CStr("!ident.delete confirm:0 ptr:%p", d) } },
258 2);
259 return iTrue;
260 }
261 deleteIdentity_GmCerts(certs_App(), menuIdentity_CertListWidget_(d));
262 postCommand_App("idents.changed");
263 return iTrue;
264 }
265 }
266 if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) {
267 const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y);
268 /* Update cursor. */
269 if (contains_Widget(w, mouse)) {
270 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
271 }
272 else if (d->contextIndex != iInvalidPos) {
273 invalidateItem_ListWidget(&d->list, d->contextIndex);
274 d->contextIndex = iInvalidPos;
275 }
276 }
277 /* Update context menu items. */
278 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) {
279 d->contextItem = NULL;
280 if (!isVisible_Widget(d->menu)) {
281 updateMouseHover_ListWidget(&d->list);
282 }
283 if (constHoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) {
284 d->contextItem = hoverItem_ListWidget(&d->list);
285 /* Context is drawn in hover state. */
286 if (d->contextIndex != iInvalidPos) {
287 invalidateItem_ListWidget(&d->list, d->contextIndex);
288 }
289 d->contextIndex = hoverItemIndex_ListWidget(&d->list);
290 updateContextMenu_CertListWidget_(d);
291 /* TODO: Some callback-based mechanism would be nice for updating menus right
292 before they open? At least move these to `updateContextMenu_ */
293 const iGmIdentity *ident = constHoverIdentity_CertListWidget(d);
294 const iString * docUrl = url_DocumentWidget(document_App());
295 iForEach(ObjectList, i, children_Widget(d->menu)) {
296 if (isInstance_Object(i.object, &Class_LabelWidget)) {
297 iLabelWidget *menuItem = i.object;
298 const char * cmdItem = cstr_String(command_LabelWidget(menuItem));
299 if (equal_Command(cmdItem, "ident.use")) {
300 const iBool cmdUse = arg_Command(cmdItem) != 0;
301 const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0;
302 setFlags_Widget(
303 as_Widget(menuItem),
304 disabled_WidgetFlag,
305 (cmdClear && !isUsed_GmIdentity(ident)) ||
306 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) ||
307 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl)));
308 }
309 }
310 }
311 }
312 if (hoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) {
313 processContextMenuEvent_Widget(d->menu, ev, {});
314 }
315 }
316 return ((iWidgetClass *) class_Widget(w)->super)->processEvent(w, ev);
317}
318
319static void draw_CertListWidget_(const iCertListWidget *d) {
320 const iWidget *w = constAs_Widget(d);
321 ((iWidgetClass *) class_Widget(w)->super)->draw(w);
322}
323
324static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect,
325 const iListWidget *list) {
326 const iCertListWidget *certList = (const iCertListWidget *) list;
327 const iBool isMenuVisible = isVisible_Widget(certList->menu);
328 const iBool isDragging = constDragItem_ListWidget(list) == d;
329 const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging;
330 const iBool isHover =
331 (!isMenuVisible &&
332 isHover_Widget(constAs_Widget(list)) &&
333 constHoverItem_ListWidget(list) == d) ||
334 (isMenuVisible && certList->contextItem == d) ||
335 isDragging;
336 const int itemHeight = height_Rect(itemRect);
337 const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId)
338 : uiIcon_ColorId;
339 const int altIconColor = isPressing ? uiTextPressed_ColorId : uiTextCaution_ColorId;
340 const int font = certList->itemFonts[d->isBold ? 1 : 0];
341 int bg = uiBackgroundSidebar_ColorId;
342 if (isHover) {
343 bg = isPressing ? uiBackgroundPressed_ColorId
344 : uiBackgroundFramelessHover_ColorId;
345 fillRect_Paint(p, itemRect, bg);
346 }
347 else if (d->listItem.isSelected) {
348 bg = uiBackgroundUnfocusedSelection_ColorId;
349 fillRect_Paint(p, itemRect, bg);
350 }
351// iInt2 pos = itemRect.pos;
352 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
353 : uiTextStrong_ColorId;
354 const iBool isUsedOnDomain = (d->indent != 0);
355 iString icon;
356 initUnicodeN_String(&icon, &d->icon, 1);
357 iInt2 cPos = topLeft_Rect(itemRect);
358 const int indent = 1.4f * lineHeight_Text(font);
359 addv_I2(&cPos,
360 init_I2(3 * gap_UI,
361 (itemHeight - lineHeight_Text(uiLabel_FontId) * 2 - lineHeight_Text(font)) /
362 2));
363 const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId
364 : uiTextFramelessHover_ColorId)
365 : uiTextDim_ColorId;
366 if (!d->listItem.isSelected && !isUsedOnDomain) {
367 drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon));
368 }
369 drawRange_Text(font,
370 cPos,
371 d->listItem.isSelected ? iconColor
372 : isUsedOnDomain ? altIconColor
373 : uiBackgroundSidebar_ColorId,
374 range_String(&icon));
375 deinit_String(&icon);
376 drawRange_Text(d->listItem.isSelected ? certList->itemFonts[1] : font,
377 add_I2(cPos, init_I2(indent, 0)),
378 fg,
379 range_String(&d->label));
380 drawRange_Text(uiLabel_FontId,
381 add_I2(cPos, init_I2(indent, lineHeight_Text(font))),
382 metaFg,
383 range_String(&d->meta));
384}
385
386void init_CertListWidget(iCertListWidget *d) {
387 iWidget *w = as_Widget(d);
388 init_ListWidget(&d->list);
389 setId_Widget(w, "certlist");
390 setBackgroundColor_Widget(w, none_ColorId);
391 d->itemFonts[0] = uiContent_FontId;
392 d->itemFonts[1] = uiContentBold_FontId;
393#if defined (iPlatformMobile)
394 if (deviceType_App() == phone_AppDeviceType) {
395 d->itemFonts[0] = uiLabelBig_FontId;
396 d->itemFonts[1] = uiLabelBigBold_FontId;
397 }
398#endif
399 d->menu = NULL;
400 d->contextItem = NULL;
401 d->contextIndex = iInvalidPos;
402}
403
404void updateItemHeight_CertListWidget(iCertListWidget *d) {
405 setItemHeight_ListWidget(&d->list, 3.5f * lineHeight_Text(d->itemFonts[0]));
406}
407
408iBool updateItems_CertListWidget(iCertListWidget *d) {
409 clear_ListWidget(&d->list);
410 destroy_Widget(d->menu);
411 d->menu = NULL;
412 const iString *tabUrl = url_DocumentWidget(document_App());
413 const iRangecc tabHost = urlHost_String(tabUrl);
414 iBool haveItems = iFalse;
415 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
416 const iGmIdentity *ident = i.ptr;
417 iCertItem *item = new_CertItem();
418 item->id = (uint32_t) index_PtrArrayConstIterator(&i);
419 item->icon = 0x1f464; /* person */
420 set_String(&item->label, name_GmIdentity(ident));
421 iDate until;
422 validUntil_TlsCertificate(ident->cert, &until);
423 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl);
424 format_String(&item->meta,
425 "%s",
426 isActive ? cstr_Lang("ident.using")
427 : isUsed_GmIdentity(ident)
428 ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls))
429 : cstr_Lang("ident.notused"));
430 const char *expiry =
431 ident->flags & temporary_GmIdentityFlag
432 ? cstr_Lang("ident.temporary")
433 : cstrCollect_String(format_Date(&until, cstr_Lang("ident.expiry")));
434 if (isEmpty_String(&ident->notes)) {
435 appendFormat_String(&item->meta, "\n%s", expiry);
436 }
437 else {
438 appendFormat_String(&item->meta,
439 " \u2014 %s\n%s%s",
440 expiry,
441 escape_Color(uiHeading_ColorId),
442 cstr_String(&ident->notes));
443 }
444 item->listItem.isSelected = isActive;
445 if (isUsedOnDomain_GmIdentity(ident, tabHost)) {
446 item->indent = 1; /* will be highlighted */
447 }
448 addItem_ListWidget(&d->list, item);
449 haveItems = iTrue;
450 iRelease(item);
451 }
452 return haveItems;
453}
454
455void deinit_CertListWidget(iCertListWidget *d) {
456 iUnused(d);
457}
458
459const iGmIdentity *constHoverIdentity_CertListWidget(const iCertListWidget *d) {
460 const iCertItem *hoverItem = constHoverItem_ListWidget(&d->list);
461 if (hoverItem) {
462 return identity_GmCerts(certs_App(), hoverItem->id);
463 }
464 return NULL;
465}
466
467iGmIdentity *hoverIdentity_CertListWidget(const iCertListWidget *d) {
468 return iConstCast(iGmIdentity *, constHoverIdentity_CertListWidget(d));
469}
470
471iBeginDefineSubclass(CertListWidget, ListWidget)
472 .processEvent = (iAny *) processEvent_CertListWidget_,
473 .draw = (iAny *) draw_CertListWidget_,
474iEndDefineSubclass(CertListWidget)