summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.c20
-rw-r--r--src/bookmarks.c30
-rw-r--r--src/bookmarks.h13
-rw-r--r--src/defs.h2
-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
10 files changed, 222 insertions, 58 deletions
diff --git a/src/app.c b/src/app.c
index 5ff93f2a..c6918eb5 100644
--- a/src/app.c
+++ b/src/app.c
@@ -2033,6 +2033,7 @@ static void resetFonts_App_(iApp *d) {
2033iBool handleCommand_App(const char *cmd) { 2033iBool handleCommand_App(const char *cmd) {
2034 iApp *d = &app_; 2034 iApp *d = &app_;
2035 const iBool isFrozen = !d->window || d->window->isDrawFrozen; 2035 const iBool isFrozen = !d->window || d->window->isDrawFrozen;
2036 /* TODO: Maybe break this up a little bit? There's a very long list of ifs here. */
2036 if (equal_Command(cmd, "config.error")) { 2037 if (equal_Command(cmd, "config.error")) {
2037 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR", 2038 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR",
2038 format_CStr("Error in config file: %s\n" 2039 format_CStr("Error in config file: %s\n"
@@ -2764,6 +2765,25 @@ iBool handleCommand_App(const char *cmd) {
2764 makeFeedSettings_Widget(findUrl_Bookmarks(d->bookmarks, url)); 2765 makeFeedSettings_Widget(findUrl_Bookmarks(d->bookmarks, url));
2765 return iTrue; 2766 return iTrue;
2766 } 2767 }
2768 else if (equal_Command(cmd, "bookmarks.addfolder")) {
2769 if (suffixPtr_Command(cmd, "value")) {
2770 add_Bookmarks(d->bookmarks, NULL, collect_String(suffix_Command(cmd, "value")), NULL, 0);
2771 postCommand_App("bookmarks.changed");
2772 }
2773 else {
2774 iWidget *dlg = makeValueInput_Widget(get_Root()->widget,
2775 collectNewCStr_String(cstr_Lang("dlg.addfolder.defaulttitle")),
2776 uiHeading_ColorEscape "${heading.addfolder}", "${dlg.addfolder.prompt}",
2777 uiTextAction_ColorEscape "${dlg.addfolder}", "bookmarks.addfolder");
2778 setSelectAllOnFocus_InputWidget(findChild_Widget(dlg, "input"), iTrue);
2779 }
2780 return iTrue;
2781 }
2782 else if (equal_Command(cmd, "bookmarks.sort")) {
2783 sort_Bookmarks(d->bookmarks, arg_Command(cmd), cmpTitleAscending_Bookmark);
2784 postCommand_App("bookmarks.changed");
2785 return iTrue;
2786 }
2767 else if (equal_Command(cmd, "bookmarks.reload.remote")) { 2787 else if (equal_Command(cmd, "bookmarks.reload.remote")) {
2768 fetchRemote_Bookmarks(bookmarks_App()); 2788 fetchRemote_Bookmarks(bookmarks_App());
2769 return iTrue; 2789 return iTrue;
diff --git a/src/bookmarks.c b/src/bookmarks.c
index 616e4632..f7691655 100644
--- a/src/bookmarks.c
+++ b/src/bookmarks.c
@@ -79,7 +79,7 @@ static int cmpTimeDescending_Bookmark_(const iBookmark **a, const iBookmark **b)
79 return iCmp(seconds_Time(&(*b)->when), seconds_Time(&(*a)->when)); 79 return iCmp(seconds_Time(&(*b)->when), seconds_Time(&(*a)->when));
80} 80}
81 81
82static int cmpTitleAscending_Bookmark_(const iBookmark **a, const iBookmark **b) { 82int cmpTitleAscending_Bookmark(const iBookmark **a, const iBookmark **b) {
83 return cmpStringCase_String(&(*a)->title, &(*b)->title); 83 return cmpStringCase_String(&(*a)->title, &(*b)->title);
84} 84}
85 85
@@ -250,7 +250,20 @@ static void load_BookmarkLoader(iBookmarkLoader *d, iFile *file) {
250iDefineTypeConstructionArgs(BookmarkLoader, (iBookmarks *b), b) 250iDefineTypeConstructionArgs(BookmarkLoader, (iBookmarks *b), b)
251 251
252/*----------------------------------------------------------------------------------------------*/ 252/*----------------------------------------------------------------------------------------------*/
253 253
254static iBool isMatchingParent_Bookmark_(void *context, const iBookmark *bm) {
255 return bm->parentId == *(const uint32_t *) context;
256}
257
258void sort_Bookmarks(iBookmarks *d, uint32_t parentId, iBookmarksCompareFunc cmp) {
259 lock_Mutex(d->mtx);
260 iConstForEach(PtrArray, i, list_Bookmarks(d, cmp, isMatchingParent_Bookmark_, &parentId)) {
261 iBookmark *bm = i.ptr;
262 bm->order = index_PtrArrayConstIterator(&i) + 1;
263 }
264 unlock_Mutex(d->mtx);
265}
266
254void load_Bookmarks(iBookmarks *d, const char *dirPath) { 267void load_Bookmarks(iBookmarks *d, const char *dirPath) {
255 clear_Bookmarks(d); 268 clear_Bookmarks(d);
256 /* Load new .ini bookmarks, if present. */ 269 /* Load new .ini bookmarks, if present. */
@@ -258,11 +271,8 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) {
258 if (!open_File(f, readOnly_FileMode | text_FileMode)) { 271 if (!open_File(f, readOnly_FileMode | text_FileMode)) {
259 /* As a fallback, try loading the v1.6 bookmarks file. */ 272 /* As a fallback, try loading the v1.6 bookmarks file. */
260 loadOldFormat_Bookmarks(d, dirPath); 273 loadOldFormat_Bookmarks(d, dirPath);
261 /* Set ordering based on titles. */ 274 /* Old format has an implicit alphabetic sort order. */
262 iConstForEach(PtrArray, i, list_Bookmarks(d, cmpTitleAscending_Bookmark_, NULL, NULL)) { 275 sort_Bookmarks(d, 0, cmpTitleAscending_Bookmark);
263 iBookmark *bm = i.ptr;
264 bm->order = index_PtrArrayConstIterator(&i) + 1;
265 }
266 return; 276 return;
267 } 277 }
268 iBookmarkLoader loader; 278 iBookmarkLoader loader;
@@ -317,7 +327,9 @@ uint32_t add_Bookmarks(iBookmarks *d, const iString *url, const iString *title,
317 iChar icon) { 327 iChar icon) {
318 lock_Mutex(d->mtx); 328 lock_Mutex(d->mtx);
319 iBookmark *bm = new_Bookmark(); 329 iBookmark *bm = new_Bookmark();
320 set_String(&bm->url, canonicalUrl_String(url)); 330 if (url) {
331 set_String(&bm->url, canonicalUrl_String(url));
332 }
321 set_String(&bm->title, title); 333 set_String(&bm->title, title);
322 if (tags) { 334 if (tags) {
323 set_String(&bm->tags, tags); 335 set_String(&bm->tags, tags);
@@ -471,7 +483,7 @@ const iString *bookmarkListPage_Bookmarks(const iBookmarks *d, enum iBookmarkLis
471 const iPtrArray *bmList = list_Bookmarks(d, 483 const iPtrArray *bmList = list_Bookmarks(d,
472 listType == listByCreationTime_BookmarkListType 484 listType == listByCreationTime_BookmarkListType
473 ? cmpTimeDescending_Bookmark_ 485 ? cmpTimeDescending_Bookmark_
474 : cmpTitleAscending_Bookmark_, 486 : cmpTitleAscending_Bookmark,
475 NULL, 487 NULL,
476 NULL); 488 NULL);
477 iConstForEach(PtrArray, i, bmList) { 489 iConstForEach(PtrArray, i, bmList) {
diff --git a/src/bookmarks.h b/src/bookmarks.h
index 0de930d7..40170062 100644
--- a/src/bookmarks.h
+++ b/src/bookmarks.h
@@ -53,7 +53,8 @@ struct Impl_Bookmark {
53 int order; /* sort order */ 53 int order; /* sort order */
54}; 54};
55 55
56iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; } 56iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; }
57iLocalDef iBool isFolder_Bookmark (const iBookmark *d) { return isEmpty_String(&d->url); }
57 58
58iBool hasTag_Bookmark (const iBookmark *, const char *tag); 59iBool hasTag_Bookmark (const iBookmark *, const char *tag);
59void addTag_Bookmark (iBookmark *, const char *tag); 60void addTag_Bookmark (iBookmark *, const char *tag);
@@ -73,11 +74,17 @@ iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add)
73 } 74 }
74} 75}
75 76
77int cmpTitleAscending_Bookmark (const iBookmark **, const iBookmark **);
78int cmpTree_Bookmark (const iBookmark **, const iBookmark **);
79
76/*----------------------------------------------------------------------------------------------*/ 80/*----------------------------------------------------------------------------------------------*/
77 81
78iDeclareType(Bookmarks) 82iDeclareType(Bookmarks)
79iDeclareTypeConstruction(Bookmarks) 83iDeclareTypeConstruction(Bookmarks)
80 84
85typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *);
86typedef int (*iBookmarksCompareFunc) (const iBookmark **, const iBookmark **);
87
81void clear_Bookmarks (iBookmarks *); 88void clear_Bookmarks (iBookmarks *);
82void load_Bookmarks (iBookmarks *, const char *dirPath); 89void load_Bookmarks (iBookmarks *, const char *dirPath);
83void save_Bookmarks (const iBookmarks *, const char *dirPath); 90void save_Bookmarks (const iBookmarks *, const char *dirPath);
@@ -88,15 +95,13 @@ iBool remove_Bookmarks (iBookmarks *, uint32_t id);
88iBookmark * get_Bookmarks (iBookmarks *, uint32_t id); 95iBookmark * get_Bookmarks (iBookmarks *, uint32_t id);
89void reorder_Bookmarks (iBookmarks *, uint32_t id, int newOrder); 96void reorder_Bookmarks (iBookmarks *, uint32_t id, int newOrder);
90iBool updateBookmarkIcon_Bookmarks(iBookmarks *, const iString *url, iChar icon); 97iBool updateBookmarkIcon_Bookmarks(iBookmarks *, const iString *url, iChar icon);
98void sort_Bookmarks (iBookmarks *, uint32_t parentId, iBookmarksCompareFunc cmp);
91void fetchRemote_Bookmarks (iBookmarks *); 99void fetchRemote_Bookmarks (iBookmarks *);
92void requestFinished_Bookmarks (iBookmarks *, iGmRequest *req); 100void requestFinished_Bookmarks (iBookmarks *, iGmRequest *req);
93 101
94iChar siteIcon_Bookmarks (const iBookmarks *, const iString *url); 102iChar siteIcon_Bookmarks (const iBookmarks *, const iString *url);
95uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */ 103uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */
96 104
97typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *);
98typedef int (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **);
99
100iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *); 105iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *);
101 106
102/** 107/**
diff --git a/src/defs.h b/src/defs.h
index 01bf2b3d..f199fd2b 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -107,6 +107,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
107#define rightArrow_Icon "\u279e" 107#define rightArrow_Icon "\u279e"
108#define barLeftArrow_Icon "\u21a4" 108#define barLeftArrow_Icon "\u21a4"
109#define barRightArrow_Icon "\u21a6" 109#define barRightArrow_Icon "\u21a6"
110#define upDownArrow_Icon "\u21c5"
110#define clock_Icon "\U0001f553" 111#define clock_Icon "\U0001f553"
111#define pin_Icon "\U0001f588" 112#define pin_Icon "\U0001f588"
112#define star_Icon "\u2605" 113#define star_Icon "\u2605"
@@ -155,6 +156,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
155#define return_Icon "\u23ce" 156#define return_Icon "\u23ce"
156#define undo_Icon "\u23ea" 157#define undo_Icon "\u23ea"
157#define select_Icon "\u2b1a" 158#define select_Icon "\u2b1a"
159#define downAngle_Icon "\ufe40"
158 160
159#if defined (iPlatformApple) 161#if defined (iPlatformApple)
160# define shift_Icon "\u21e7" 162# define shift_Icon "\u21e7"
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};