summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-09-24 16:00:22 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-09-24 16:00:22 +0300
commit53f0596cfd6060cd63cc8e6f92f981672da97ee2 (patch)
tree313eaa75bf3311e368678bac70871d2059abc14a /src/ui
parent446df26adb969dcb4dd9cf5e171fbb9453a8e27c (diff)
Bookmark folders
The user can now create bookmark folders, and drag bookmarks into them. Folders can also be nested. The bookmark sorting menu item sorts inside the chosen folder/root. Remote bookmark sources appear also as folders, although they cannot be sorted/edited. IssueID #339
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/keys.c1
-rw-r--r--src/ui/listwidget.c30
-rw-r--r--src/ui/lookupwidget.c3
-rw-r--r--src/ui/sidebarwidget.c170
-rw-r--r--src/ui/util.c8
-rw-r--r--src/ui/window.c3
6 files changed, 170 insertions, 45 deletions
diff --git a/src/ui/keys.c b/src/ui/keys.c
index 6de30f57..30072572 100644
--- a/src/ui/keys.c
+++ b/src/ui/keys.c
@@ -213,6 +213,7 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =
213 { 46, { "${keys.link.homerow.hover}", 'h', 0, "document.linkkeys arg:1 hover:1" }, 0 }, 213 { 46, { "${keys.link.homerow.hover}", 'h', 0, "document.linkkeys arg:1 hover:1" }, 0 },
214 { 47, { "${keys.link.homerow.next}", '.', 0, "document.linkkeys more:1" }, 0 }, 214 { 47, { "${keys.link.homerow.next}", '.', 0, "document.linkkeys more:1" }, 0 },
215 { 50, { "${keys.bookmark.add}", 'd', KMOD_PRIMARY, "bookmark.add" }, 0 }, 215 { 50, { "${keys.bookmark.add}", 'd', KMOD_PRIMARY, "bookmark.add" }, 0 },
216 { 51, { "${keys.bookmark.addfolder}", 'n', KMOD_SHIFT, "bookmarks.addfolder" }, 0 },
216 { 55, { "${keys.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 0 }, 217 { 55, { "${keys.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 0 },
217 { 60, { "${keys.findtext}", 'f', KMOD_PRIMARY, "focus.set id:find.input" }, 0 }, 218 { 60, { "${keys.findtext}", 'f', KMOD_PRIMARY, "focus.set id:find.input" }, 0 },
218 { 70, { "${keys.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, 0 }, 219 { 70, { "${keys.zoom.in}", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, 0 },
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c
index a34f3d03..f896b493 100644
--- a/src/ui/listwidget.c
+++ b/src/ui/listwidget.c
@@ -331,8 +331,8 @@ static size_t resolveDragDestination_ListWidget_(const iListWidget *d, iInt2 dst
331 const iRect rect = itemRect_ListWidget(d, index); 331 const iRect rect = itemRect_ListWidget(d, index);
332 const iRangei span = ySpan_Rect(rect); 332 const iRangei span = ySpan_Rect(rect);
333 if (item->isDropTarget) { 333 if (item->isDropTarget) {
334 const int pad = size_Range(&span) / 4; 334 const int pad = size_Range(&span) / 3;
335 if (dstPos.y >= span.start + pad && dstPos.y < span.end) { 335 if (dstPos.y >= span.start + pad && dstPos.y < span.end - pad) {
336 *isOnto = iTrue; 336 *isOnto = iTrue;
337 return index; 337 return index;
338 } 338 }
@@ -352,11 +352,13 @@ static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) {
352 stop_Anim(&d->scrollY.pos); 352 stop_Anim(&d->scrollY.pos);
353 iBool isOnto; 353 iBool isOnto;
354 const size_t index = resolveDragDestination_ListWidget_(d, endPos, &isOnto); 354 const size_t index = resolveDragDestination_ListWidget_(d, endPos, &isOnto);
355 if (isOnto) { 355 if (index != d->dragItem) {
356 postCommand_Widget(d, "list.dragged arg:%zu onto:%zu", d->dragItem, index); 356 if (isOnto) {
357 } 357 postCommand_Widget(d, "list.dragged arg:%zu onto:%zu", d->dragItem, index);
358 else if (index != d->dragItem) { 358 }
359 postCommand_Widget(d, "list.dragged arg:%zu before:%zu", d->dragItem, index); 359 else {
360 postCommand_Widget(d, "list.dragged arg:%zu before:%zu", d->dragItem, index);
361 }
360 } 362 }
361 invalidateItem_ListWidget(d, d->dragItem); 363 invalidateItem_ListWidget(d, d->dragItem);
362 d->dragItem = iInvalidPos; 364 d->dragItem = iInvalidPos;
@@ -394,7 +396,7 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
394 } 396 }
395 else if (d->dragItem != iInvalidPos) { 397 else if (d->dragItem != iInvalidPos) {
396 /* Start scrolling if near the ends. */ 398 /* Start scrolling if near the ends. */
397 const int zone = d->itemHeight; 399 const int zone = 2 * d->itemHeight;
398 const iRect bounds = bounds_Widget(w); 400 const iRect bounds = bounds_Widget(w);
399 float scrollSpeed = 0.0f; 401 float scrollSpeed = 0.0f;
400 if (mousePos.y > bottom_Rect(bounds) - zone) { 402 if (mousePos.y > bottom_Rect(bounds) - zone) {
@@ -410,7 +412,7 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
410 } 412 }
411 else { 413 else {
412 setValueSpeed_Anim(&d->scrollY.pos, scrollSpeed < 0 ? 0 : scrollMax_ListWidget_(d), 414 setValueSpeed_Anim(&d->scrollY.pos, scrollSpeed < 0 ? 0 : scrollMax_ListWidget_(d),
413 iAbs(scrollSpeed * gap_UI * 100)); 415 scrollSpeed * scrollSpeed * gap_UI * 400);
414 refreshWhileScrolling_ListWidget_(d); 416 refreshWhileScrolling_ListWidget_(d);
415 } 417 }
416 } 418 }
@@ -451,7 +453,7 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
451 ((const iListItem *) item_ListWidget(d, over))->isDraggable) { 453 ((const iListItem *) item_ListWidget(d, over))->isDraggable) {
452 d->dragItem = over; 454 d->dragItem = over;
453 d->dragOrigin = sub_I2(topLeft_Rect(itemRect_ListWidget(d, over)), 455 d->dragOrigin = sub_I2(topLeft_Rect(itemRect_ListWidget(d, over)),
454 pos_Click(&d->click)); 456 d->click.startPos);
455 invalidateItem_ListWidget(d, d->dragItem); 457 invalidateItem_ListWidget(d, d->dragItem);
456 } 458 }
457 } 459 }
@@ -569,20 +571,22 @@ static void draw_ListWidget_(const iListWidget *d) {
569 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); 571 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
570 iBool dstOnto; 572 iBool dstOnto;
571 const size_t dstIndex = resolveDragDestination_ListWidget_(d, mousePos, &dstOnto); 573 const size_t dstIndex = resolveDragDestination_ListWidget_(d, mousePos, &dstOnto);
572 if (dstIndex != d->dragItem && dstIndex != d->dragItem + 1) { 574 if (dstIndex != d->dragItem) {
573 const iRect dstRect = itemRect_ListWidget(d, dstIndex); 575 const iRect dstRect = itemRect_ListWidget(d, dstIndex);
574 p.alpha = 0xff; 576 p.alpha = 0xff;
575 if (dstOnto) { 577 if (dstOnto) {
576 fillRect_Paint(&p, dstRect, uiTextAction_ColorId); 578 drawRectThickness_Paint(&p, dstRect, gap_UI / 2, uiTextAction_ColorId);
577 } 579 }
578 else { 580 else if (dstIndex != d->dragItem + 1) {
579 fillRect_Paint(&p, (iRect){ addY_I2(dstRect.pos, -gap_UI / 4), 581 fillRect_Paint(&p, (iRect){ addY_I2(dstRect.pos, -gap_UI / 4),
580 init_I2(width_Rect(dstRect), gap_UI / 2) }, 582 init_I2(width_Rect(dstRect), gap_UI / 2) },
581 uiTextAction_ColorId); 583 uiTextAction_ColorId);
582 } 584 }
583 } 585 }
584 p.alpha = 0x80; 586 p.alpha = 0x80;
587 setOpacity_Text(0.5f);
585 class_ListItem(item)->draw(item, &p, itemRect, d); 588 class_ListItem(item)->draw(item, &p, itemRect, d);
589 setOpacity_Text(1.0f);
586 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); 590 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
587 } 591 }
588 unsetClip_Paint(&p); 592 unsetClip_Paint(&p);
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index 85217336..ab649eee 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -171,6 +171,9 @@ static float scoreMatch_(const iRegExp *pattern, iRangecc text) {
171} 171}
172 172
173static float bookmarkRelevance_LookupJob_(const iLookupJob *d, const iBookmark *bm) { 173static float bookmarkRelevance_LookupJob_(const iLookupJob *d, const iBookmark *bm) {
174 if (isFolder_Bookmark(bm)) {
175 return 0.0f;
176 }
174 iUrl parts; 177 iUrl parts;
175 init_Url(&parts, &bm->url); 178 init_Url(&parts, &bm->url);
176 const float t = scoreMatch_(d->term, range_String(&bm->title)); 179 const float t = scoreMatch_(d->term, range_String(&bm->title));
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 20e43153..6c2934ec 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -108,6 +108,7 @@ struct Impl_SidebarWidget {
108 iWidget * menu; 108 iWidget * menu;
109 iSidebarItem * contextItem; /* list item accessed in the context menu */ 109 iSidebarItem * contextItem; /* list item accessed in the context menu */
110 size_t contextIndex; /* index of list item accessed in the context menu */ 110 size_t contextIndex; /* index of list item accessed in the context menu */
111 iIntSet closedFolders; /* otherwise open */
111}; 112};
112 113
113iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side) 114iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side)
@@ -116,26 +117,67 @@ static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) {
116 return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; 117 return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0;
117} 118}
118 119
119static int cmpTree_Bookmark_(const iBookmark **a, const iBookmark **b) { 120iBookmark *parent_Bookmark(const iBookmark *d) {
121 /* TODO: Parent pointers should be prefetched! */
122 if (d->parentId) {
123 return get_Bookmarks(bookmarks_App(), d->parentId);
124 }
125 return NULL;
126}
127
128iBool hasParent_Bookmark(const iBookmark *d, uint32_t parentId) {
129 /* TODO: Parent pointers should be prefetched! */
130 while (d->parentId) {
131 if (d->parentId == parentId) {
132 return iTrue;
133 }
134 d = get_Bookmarks(bookmarks_App(), d->parentId);
135 }
136 return iFalse;
137}
138
139int depth_Bookmark(const iBookmark *d) {
140 /* TODO: Precalculate this! */
141 int depth = 0;
142 for (; d->parentId; depth++) {
143 d = get_Bookmarks(bookmarks_App(), d->parentId);
144 }
145 return depth;
146}
147
148int cmpTree_Bookmark(const iBookmark **a, const iBookmark **b) {
120 const iBookmark *bm1 = *a, *bm2 = *b; 149 const iBookmark *bm1 = *a, *bm2 = *b;
121 if (bm2->parentId == id_Bookmark(bm1)) { 150 /* Contents of a parent come after it. */
151 if (hasParent_Bookmark(bm2, id_Bookmark(bm1))) {
122 return -1; 152 return -1;
123 } 153 }
124 if (bm1->parentId == id_Bookmark(bm2)) { 154 if (hasParent_Bookmark(bm1, id_Bookmark(bm2))) {
125 return 1; 155 return 1;
126 } 156 }
127 if (bm1->parentId == bm2->parentId) { 157 /* Comparisons are only valid inside the same parent. */
128 //return cmpStringCase_String(&bm1->title, &bm2->title); 158 while (bm1->parentId != bm2->parentId) {
129 return iCmp(bm1->order, bm2->order); 159 int depth1 = depth_Bookmark(bm1);
130 } 160 int depth2 = depth_Bookmark(bm2);
131 if (bm1->parentId) { 161 if (depth1 != depth2) {
132 bm1 = get_Bookmarks(bookmarks_App(), bm1->parentId); 162 /* Equalize the depth. */
133 } 163 while (depth1 > depth2) {
134 if (bm2->parentId) { 164 bm1 = parent_Bookmark(bm1);
135 bm2 = get_Bookmarks(bookmarks_App(), bm2->parentId); 165 depth1--;
166 }
167 while (depth2 > depth1) {
168 bm2 = parent_Bookmark(bm2);
169 depth2--;
170 }
171 continue;
172 }
173 bm1 = parent_Bookmark(bm1);
174 depth1--;
175 bm2 = parent_Bookmark(bm2);
176 depth2--;
136 } 177 }
137// return cmpStringCase_String(&bm1->title, &bm2->title); 178 const int cmp = iCmp(bm1->order, bm2->order);
138 return iCmp(bm1->order, bm2->order); 179 if (cmp) return cmp;
180 return cmpStringCase_String(&bm1->title, &bm2->title);
139} 181}
140 182
141static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const char *label, 183static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const char *label,
@@ -211,6 +253,16 @@ static void updateContextMenu_SidebarWidget_(iSidebarWidget *d) {
211 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items)); 253 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
212} 254}
213 255
256static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBookmark *bm) {
257 while (bm->parentId) {
258 if (contains_IntSet(&d->closedFolders, bm->parentId)) {
259 return iTrue;
260 }
261 bm = get_Bookmarks(bookmarks_App(), bm->parentId);
262 }
263 return iFalse;
264}
265
214static void updateItems_SidebarWidget_(iSidebarWidget *d) { 266static void updateItems_SidebarWidget_(iSidebarWidget *d) {
215 clear_ListWidget(d->list); 267 clear_ListWidget(d->list);
216 releaseChildren_Widget(d->blank); 268 releaseChildren_Widget(d->blank);
@@ -334,12 +386,22 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
334 iRegExp *remoteSourceTag = iClob(new_RegExp("\\b" remoteSource_BookmarkTag "\\b", caseSensitive_RegExpOption)); 386 iRegExp *remoteSourceTag = iClob(new_RegExp("\\b" remoteSource_BookmarkTag "\\b", caseSensitive_RegExpOption));
335 iRegExp *remoteTag = iClob(new_RegExp("\\b" remote_BookmarkTag "\\b", caseSensitive_RegExpOption)); 387 iRegExp *remoteTag = iClob(new_RegExp("\\b" remote_BookmarkTag "\\b", caseSensitive_RegExpOption));
336 iRegExp *linkSplitTag = iClob(new_RegExp("\\b" linkSplit_BookmarkTag "\\b", caseSensitive_RegExpOption)); 388 iRegExp *linkSplitTag = iClob(new_RegExp("\\b" linkSplit_BookmarkTag "\\b", caseSensitive_RegExpOption));
337 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark_, NULL, NULL)) { 389 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark, NULL, NULL)) {
338 const iBookmark *bm = i.ptr; 390 const iBookmark *bm = i.ptr;
391 if (isBookmarkFolded_SidebarWidget_(d, bm)) {
392 continue; /* inside a closed folder */
393 }
339 iSidebarItem *item = new_SidebarItem(); 394 iSidebarItem *item = new_SidebarItem();
340 item->listItem.isDraggable = iTrue; 395 item->listItem.isDraggable = iTrue;
396 item->isBold = item->listItem.isDropTarget = isFolder_Bookmark(bm);
341 item->id = id_Bookmark(bm); 397 item->id = id_Bookmark(bm);
342 item->icon = bm->icon; 398 item->indent = depth_Bookmark(bm);
399 if (isFolder_Bookmark(bm)) {
400 item->icon = contains_IntSet(&d->closedFolders, item->id) ? 0x27e9 : 0xfe40;
401 }
402 else {
403 item->icon = bm->icon;
404 }
343 set_String(&item->url, &bm->url); 405 set_String(&item->url, &bm->url);
344 set_String(&item->label, &bm->title); 406 set_String(&item->label, &bm->title);
345 /* Icons for special tags. */ { 407 /* Icons for special tags. */ {
@@ -384,8 +446,11 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
384 { "---", 0, 0, NULL }, 446 { "---", 0, 0, NULL },
385 { delete_Icon " " uiTextCaution_ColorEscape "${bookmark.delete}", 0, 0, "bookmark.delete" }, 447 { delete_Icon " " uiTextCaution_ColorEscape "${bookmark.delete}", 0, 0, "bookmark.delete" },
386 { "---", 0, 0, NULL }, 448 { "---", 0, 0, NULL },
387 { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } }, 449 { add_Icon " ${menu.newfolder}", 0, 0, "bookmarks.addfolder" },
388 14); 450 { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" },
451 { "---", 0, 0, NULL },
452 { upDownArrow_Icon " ${menu.sort.alpha}", 0, 0, "bookmark.sortfolder" } },
453 17);
389 break; 454 break;
390 } 455 }
391 case history_SidebarMode: { 456 case history_SidebarMode: {
@@ -671,6 +736,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
671 d->resizer = NULL; 736 d->resizer = NULL;
672 d->list = NULL; 737 d->list = NULL;
673 d->actions = NULL; 738 d->actions = NULL;
739 init_IntSet(&d->closedFolders);
674 /* On a phone, the right sidebar is used exclusively for Identities. */ 740 /* On a phone, the right sidebar is used exclusively for Identities. */
675 const iBool isPhone = deviceType_App() == phone_AppDeviceType; 741 const iBool isPhone = deviceType_App() == phone_AppDeviceType;
676 if (!isPhone || d->side == left_SidebarSide) { 742 if (!isPhone || d->side == left_SidebarSide) {
@@ -754,6 +820,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
754 820
755void deinit_SidebarWidget(iSidebarWidget *d) { 821void deinit_SidebarWidget(iSidebarWidget *d) {
756 deinit_String(&d->cmdPrefix); 822 deinit_String(&d->cmdPrefix);
823 deinit_IntSet(&d->closedFolders);
757} 824}
758 825
759iBool setButtonFont_SidebarWidget(iSidebarWidget *d, int font) { 826iBool setButtonFont_SidebarWidget(iSidebarWidget *d, int font) {
@@ -801,6 +868,17 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
801 break; 868 break;
802 } 869 }
803 case bookmarks_SidebarMode: 870 case bookmarks_SidebarMode:
871 if (isEmpty_String(&item->url)) /* a folder */ {
872 if (contains_IntSet(&d->closedFolders, item->id)) {
873 remove_IntSet(&d->closedFolders, item->id);
874 }
875 else {
876 insert_IntSet(&d->closedFolders, item->id);
877 }
878 updateItems_SidebarWidget_(d);
879 break;
880 }
881 /* fall through */
804 case history_SidebarMode: { 882 case history_SidebarMode: {
805 if (!isEmpty_String(&item->url)) { 883 if (!isEmpty_String(&item->url)) {
806 postCommandf_Root(get_Root(), "open fromsidebar:1 newtab:%d url:%s", 884 postCommandf_Root(get_Root(), "open fromsidebar:1 newtab:%d url:%s",
@@ -1013,10 +1091,24 @@ static void bookmarkMoved_SidebarWidget_(iSidebarWidget *d, size_t index, size_t
1013 isLast ? numItems_ListWidget(d->list) - 1 1091 isLast ? numItems_ListWidget(d->list) - 1
1014 : beforeIndex); 1092 : beforeIndex);
1015 const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id); 1093 const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id);
1094 if (hasParent_Bookmark(dst, movingItem->id)) {
1095 return;
1096 }
1016 reorder_Bookmarks(bookmarks_App(), movingItem->id, dst->order + (isLast ? 1 : 0)); 1097 reorder_Bookmarks(bookmarks_App(), movingItem->id, dst->order + (isLast ? 1 : 0));
1098 get_Bookmarks(bookmarks_App(), movingItem->id)->parentId = dst->parentId;
1017 updateItems_SidebarWidget_(d); 1099 updateItems_SidebarWidget_(d);
1018 /* Don't confuse the user: keep the dragged item in hover state. */ 1100 /* Don't confuse the user: keep the dragged item in hover state. */
1019 setHoverItem_ListWidget(d->list, index < beforeIndex ? beforeIndex - 1 : beforeIndex); 1101 setHoverItem_ListWidget(d->list, index < beforeIndex ? beforeIndex - 1 : beforeIndex);
1102 postCommandf_App("bookmarks.changed nosidebar:%p", d); /* skip this sidebar since we updated already */
1103}
1104
1105static void bookmarkMovedOntoFolder_SidebarWidget_(iSidebarWidget *d, size_t index,
1106 size_t folderIndex) {
1107 const iSidebarItem *movingItem = item_ListWidget(d->list, index);
1108 const iSidebarItem *dstItem = item_ListWidget(d->list, folderIndex);
1109 iBookmark *bm = get_Bookmarks(bookmarks_App(), movingItem->id);
1110 bm->parentId = dstItem->id;
1111 postCommand_App("bookmarks.changed");
1020} 1112}
1021 1113
1022static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { 1114static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) {
@@ -1070,7 +1162,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1070 } 1162 }
1071 else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode || 1163 else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode ||
1072 d->mode == feeds_SidebarMode)) { 1164 d->mode == feeds_SidebarMode)) {
1073 updateItems_SidebarWidget_(d); 1165 if (pointerLabel_Command(cmd, "nosidebar") != d) {
1166 updateItems_SidebarWidget_(d);
1167 }
1074 } 1168 }
1075 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) { 1169 else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) {
1076 updateItems_SidebarWidget_(d); 1170 updateItems_SidebarWidget_(d);
@@ -1135,6 +1229,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1135 } 1229 }
1136 else { 1230 else {
1137 /* Dragged onto a folder. */ 1231 /* Dragged onto a folder. */
1232 bookmarkMovedOntoFolder_SidebarWidget_(d,
1233 argU32Label_Command(cmd, "arg"),
1234 argU32Label_Command(cmd, "onto"));
1138 } 1235 }
1139 return iTrue; 1236 return iTrue;
1140 } 1237 }
@@ -1170,15 +1267,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1170 setText_InputWidget(findChild_Widget(dlg, "bmed.icon"), 1267 setText_InputWidget(findChild_Widget(dlg, "bmed.icon"),
1171 collect_String(newUnicodeN_String(&bm->icon, 1))); 1268 collect_String(newUnicodeN_String(&bm->icon, 1)));
1172 } 1269 }
1173 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.home"), 1270 setToggle_Widget(findChild_Widget(dlg, "bmed.tag.home"),
1174 selected_WidgetFlag, 1271 hasTag_Bookmark(bm, homepage_BookmarkTag));
1175 hasTag_Bookmark(bm, homepage_BookmarkTag)); 1272 setToggle_Widget(findChild_Widget(dlg, "bmed.tag.remote"),
1176 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.remote"), 1273 hasTag_Bookmark(bm, remoteSource_BookmarkTag));
1177 selected_WidgetFlag, 1274 setToggle_Widget(findChild_Widget(dlg, "bmed.tag.linksplit"),
1178 hasTag_Bookmark(bm, remoteSource_BookmarkTag)); 1275 hasTag_Bookmark(bm, linkSplit_BookmarkTag));
1179 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.linksplit"),
1180 selected_WidgetFlag,
1181 hasTag_Bookmark(bm, linkSplit_BookmarkTag));
1182 setCommandHandler_Widget(dlg, handleBookmarkEditorCommands_SidebarWidget_); 1276 setCommandHandler_Widget(dlg, handleBookmarkEditorCommands_SidebarWidget_);
1183 setFocus_Widget(findChild_Widget(dlg, "bmed.title")); 1277 setFocus_Widget(findChild_Widget(dlg, "bmed.title"));
1184 } 1278 }
@@ -1225,6 +1319,16 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1225 } 1319 }
1226 return iTrue; 1320 return iTrue;
1227 } 1321 }
1322 else if (isCommand_Widget(w, ev, "bookmark.sortfolder")) {
1323 const iSidebarItem *item = d->contextItem;
1324 if (d->mode == bookmarks_SidebarMode && item) {
1325 postCommandf_App("bookmarks.sort arg:%zu",
1326 item->listItem.isDropTarget
1327 ? item->id
1328 : get_Bookmarks(bookmarks_App(), item->id)->parentId);
1329 }
1330 return iTrue;
1331 }
1228 else if (equal_Command(cmd, "feeds.update.finished")) { 1332 else if (equal_Command(cmd, "feeds.update.finished")) {
1229 d->numUnreadEntries = argLabel_Command(cmd, "unread"); 1333 d->numUnreadEntries = argLabel_Command(cmd, "unread");
1230 checkModeButtonLayout_SidebarWidget_(d); 1334 checkModeButtonLayout_SidebarWidget_(d);
@@ -1638,7 +1742,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1638 fillRect_Paint(p, itemRect, bg); 1742 fillRect_Paint(p, itemRect, bg);
1639 } 1743 }
1640 else if (sidebar->mode == bookmarks_SidebarMode) { 1744 else if (sidebar->mode == bookmarks_SidebarMode) {
1641 if (d->icon == 0x2913) { /* TODO: Remote icon; meaning: is this in a folder? */ 1745 if (d->indent) /* remote icon */ {
1642 bg = uiBackgroundFolder_ColorId; 1746 bg = uiBackgroundFolder_ColorId;
1643 fillRect_Paint(p, itemRect, bg); 1747 fillRect_Paint(p, itemRect, bg);
1644 } 1748 }
@@ -1740,11 +1844,13 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1740 } 1844 }
1741 else if (sidebar->mode == bookmarks_SidebarMode) { 1845 else if (sidebar->mode == bookmarks_SidebarMode) {
1742 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) 1846 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
1743 : uiText_ColorId; 1847 : d->listItem.isDropTarget ? uiHeading_ColorId : uiText_ColorId;
1848 /* The icon. */
1744 iString str; 1849 iString str;
1745 init_String(&str); 1850 init_String(&str);
1746 appendChar_String(&str, d->icon ? d->icon : 0x1f588); 1851 appendChar_String(&str, d->icon ? d->icon : 0x1f588);
1747 const iRect iconArea = { addX_I2(pos, gap_UI), 1852 const int leftIndent = d->indent * gap_UI * 4;
1853 const iRect iconArea = { addX_I2(pos, gap_UI + leftIndent),
1748 init_I2(1.75f * lineHeight_Text(font), itemHeight) }; 1854 init_I2(1.75f * lineHeight_Text(font), itemHeight) };
1749 drawCentered_Text(font, 1855 drawCentered_Text(font,
1750 iconArea, 1856 iconArea,
diff --git a/src/ui/util.c b/src/ui/util.c
index 7fa5d675..5b9f15a9 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -816,6 +816,14 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
816 setUserData_Object(menu, deepCopyMenuItems_(menu, items, n)); 816 setUserData_Object(menu, deepCopyMenuItems_(menu, items, n));
817 addChild_Widget(parent, menu); 817 addChild_Widget(parent, menu);
818 iRelease(menu); /* owned by parent now */ 818 iRelease(menu); /* owned by parent now */
819 /* Keyboard shortcuts still need to triggerable via the menu, although the items don't exist. */ {
820 for (size_t i = 0; i < n; i++) {
821 const iMenuItem *item = &items[i];
822 if (item->key) {
823 addAction_Widget(menu, item->key, item->kmods, item->command);
824 }
825 }
826 }
819#else 827#else
820 /* Non-native custom popup menu. This may still be displayed inside a separate window. */ 828 /* Non-native custom popup menu. This may still be displayed inside a separate window. */
821 setDrawBufferEnabled_Widget(menu, iTrue); 829 setDrawBufferEnabled_Widget(menu, iTrue);
diff --git a/src/ui/window.c b/src/ui/window.c
index 0863aa47..5941ef5f 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -120,6 +120,7 @@ static const iMenuItem viewMenuItems_[] = {
120static iMenuItem bookmarksMenuItems_[] = { 120static iMenuItem bookmarksMenuItems_[] = {
121 { "${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 121 { "${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
122 { "${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, 122 { "${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
123 { "${menu.newfolder}", 0, 0, "bookmarks.addfolder" },
123 { "---", 0, 0, NULL }, 124 { "---", 0, 0, NULL },
124 { "${menu.import.links}", 0, 0, "bookmark.links confirm:1" }, 125 { "${menu.import.links}", 0, 0, "bookmark.links confirm:1" },
125 { "---", 0, 0, NULL }, 126 { "---", 0, 0, NULL },
@@ -128,6 +129,8 @@ static iMenuItem bookmarksMenuItems_[] = {
128 { "${macos.menu.bookmarks.bytime}", 0, 0, "open url:about:bookmarks?created" }, 129 { "${macos.menu.bookmarks.bytime}", 0, 0, "open url:about:bookmarks?created" },
129 { "${menu.feeds.entrylist}", 0, 0, "open url:about:feeds" }, 130 { "${menu.feeds.entrylist}", 0, 0, "open url:about:feeds" },
130 { "---", 0, 0, NULL }, 131 { "---", 0, 0, NULL },
132 { "${menu.sort.alpha}", 0, 0, "bookmarks.sort" },
133 { "---", 0, 0, NULL },
131 { "${menu.bookmarks.refresh}", 0, 0, "bookmarks.reload.remote" }, 134 { "${menu.bookmarks.refresh}", 0, 0, "bookmarks.reload.remote" },
132 { "${menu.feeds.refresh}", SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh" }, 135 { "${menu.feeds.refresh}", SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh" },
133}; 136};