Lagrange [work/v1.7]

Bookmark folders

=> 53f0596cfd6060cd63cc8e6f92f981672da97ee2

diff --git a/po/compile.py b/po/compile.py
index 178342a6..2b0273b6 100755
--- a/po/compile.py
+++ b/po/compile.py
@@ -101,7 +101,9 @@ def parse_po(src):
 def compile_string(msg_id, msg_str):
     return msg_id.encode('utf-8') + bytes([0]) + \
            msg_str.encode('utf-8') + bytes([0])
-                          
+    
+
+os.chdir(os.path.dirname(__file__))
     
 if MODE == 'compile':
     BASE_STRINGS = {}
diff --git a/po/en.po b/po/en.po
index 63aeb75f..24525b03 100644
--- a/po/en.po
+++ b/po/en.po
@@ -225,7 +225,13 @@ msgid "menu.zoom.reset"
 msgstr "Reset Zoom"
 
 msgid "menu.view.split"
-msgstr "Split View..."
+msgstr "Split View…"
+
+msgid "menu.newfolder"
+msgstr "New Folder…"
+
+msgid "menu.sort.alpha"
+msgstr "Sort Alphabetically"
 
 msgid "menu.bookmarks.list"
 msgstr "List All Bookmarks"
@@ -1189,6 +1195,18 @@ msgstr "Icon:"
 msgid "heading.bookmark.tags"
 msgstr "SPECIAL TAGS"
 
+msgid "heading.addfolder"
+msgstr "ADD FOLDER"
+
+msgid "dlg.addfolder.defaulttitle"
+msgstr "New Folder"
+
+msgid "dlg.addfolder.prompt"
+msgstr "Enter the name of the new folder:"
+
+msgid "dlg.addfolder"
+msgstr "Add Folder"
+
 msgid "heading.prefs"
 msgstr "PREFERENCES"
 
@@ -1564,6 +1582,9 @@ msgstr "Next set of home row key links"
 msgid "keys.bookmark.add"
 msgstr "Add bookmark"
 
+msgid "keys.bookmark.addfolder"
+msgstr "Add bookmark folder"
+
 msgid "keys.subscribe"
 msgstr "Subscribe to page"
 
diff --git a/res/lang/de.bin b/res/lang/de.bin
index 060745b2..4d48c61f 100644
Binary files a/res/lang/de.bin and b/res/lang/de.bin differ
diff --git a/res/lang/en.bin b/res/lang/en.bin
index d30b1cd3..8782920f 100644
Binary files a/res/lang/en.bin and b/res/lang/en.bin differ
diff --git a/res/lang/es.bin b/res/lang/es.bin
index c03b6960..fa86ea3a 100644
Binary files a/res/lang/es.bin and b/res/lang/es.bin differ
diff --git a/res/lang/fi.bin b/res/lang/fi.bin
index 4898c850..f23cbf09 100644
Binary files a/res/lang/fi.bin and b/res/lang/fi.bin differ
diff --git a/res/lang/fr.bin b/res/lang/fr.bin
index 65a17bfe..d4c5cf55 100644
Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ
diff --git a/res/lang/ia.bin b/res/lang/ia.bin
index 93904f71..ee6533ca 100644
Binary files a/res/lang/ia.bin and b/res/lang/ia.bin differ
diff --git a/res/lang/ie.bin b/res/lang/ie.bin
index 769f38b1..2bbc7ada 100644
Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ
diff --git a/res/lang/pl.bin b/res/lang/pl.bin
index 36c6e552..651a6231 100644
Binary files a/res/lang/pl.bin and b/res/lang/pl.bin differ
diff --git a/res/lang/ru.bin b/res/lang/ru.bin
index e1479727..1a3f7213 100644
Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ
diff --git a/res/lang/sr.bin b/res/lang/sr.bin
index 90e04616..184699f1 100644
Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ
diff --git a/res/lang/tok.bin b/res/lang/tok.bin
index b0be44d4..a77aa161 100644
Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ
diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin
index d2685429..1fe4392d 100644
Binary files a/res/lang/zh_Hans.bin and b/res/lang/zh_Hans.bin differ
diff --git a/res/lang/zh_Hant.bin b/res/lang/zh_Hant.bin
index 5319e518..244bb3a1 100644
Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ
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) {
 iBool handleCommand_App(const char *cmd) {
     iApp *d = &app_;
     const iBool isFrozen = !d->window || d->window->isDrawFrozen;
+    /* TODO: Maybe break this up a little bit? There's a very long list of ifs here. */
     if (equal_Command(cmd, "config.error")) {
         makeSimpleMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR",
                                  format_CStr("Error in config file: %s\n"
@@ -2764,6 +2765,25 @@ iBool handleCommand_App(const char *cmd) {
         makeFeedSettings_Widget(findUrl_Bookmarks(d->bookmarks, url));
         return iTrue;
     }
+    else if (equal_Command(cmd, "bookmarks.addfolder")) {
+        if (suffixPtr_Command(cmd, "value")) {
+            add_Bookmarks(d->bookmarks, NULL, collect_String(suffix_Command(cmd, "value")), NULL, 0);
+            postCommand_App("bookmarks.changed");
+        }
+        else {
+            iWidget *dlg = makeValueInput_Widget(get_Root()->widget,
+                                                 collectNewCStr_String(cstr_Lang("dlg.addfolder.defaulttitle")),
+                                                 uiHeading_ColorEscape "${heading.addfolder}", "${dlg.addfolder.prompt}",
+                                                 uiTextAction_ColorEscape "${dlg.addfolder}", "bookmarks.addfolder");
+            setSelectAllOnFocus_InputWidget(findChild_Widget(dlg, "input"), iTrue);
+        }
+        return iTrue;
+    }
+    else if (equal_Command(cmd, "bookmarks.sort")) {
+        sort_Bookmarks(d->bookmarks, arg_Command(cmd), cmpTitleAscending_Bookmark);
+        postCommand_App("bookmarks.changed");
+        return iTrue;
+    }
     else if (equal_Command(cmd, "bookmarks.reload.remote")) {
         fetchRemote_Bookmarks(bookmarks_App());
         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)
     return iCmp(seconds_Time(&(*b)->when), seconds_Time(&(*a)->when));
 }
 
-static int cmpTitleAscending_Bookmark_(const iBookmark **a, const iBookmark **b) {
+int cmpTitleAscending_Bookmark(const iBookmark **a, const iBookmark **b) {
     return cmpStringCase_String(&(*a)->title, &(*b)->title);
 }
 
@@ -250,7 +250,20 @@ static void load_BookmarkLoader(iBookmarkLoader *d, iFile *file) {
 iDefineTypeConstructionArgs(BookmarkLoader, (iBookmarks *b), b)
     
 /*----------------------------------------------------------------------------------------------*/
-    
+
+static iBool isMatchingParent_Bookmark_(void *context, const iBookmark *bm) {
+    return bm->parentId == *(const uint32_t *) context;
+}
+
+void sort_Bookmarks(iBookmarks *d, uint32_t parentId, iBookmarksCompareFunc cmp) {
+    lock_Mutex(d->mtx);
+    iConstForEach(PtrArray, i, list_Bookmarks(d, cmp, isMatchingParent_Bookmark_, &parentId)) {
+        iBookmark *bm = i.ptr;
+        bm->order = index_PtrArrayConstIterator(&i) + 1;
+    }
+    unlock_Mutex(d->mtx);
+}
+
 void load_Bookmarks(iBookmarks *d, const char *dirPath) {
     clear_Bookmarks(d);
     /* Load new .ini bookmarks, if present. */
@@ -258,11 +271,8 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) {
     if (!open_File(f, readOnly_FileMode | text_FileMode)) {
         /* As a fallback, try loading the v1.6 bookmarks file. */
         loadOldFormat_Bookmarks(d, dirPath);
-        /* Set ordering based on titles. */
-        iConstForEach(PtrArray, i, list_Bookmarks(d, cmpTitleAscending_Bookmark_, NULL, NULL)) {
-            iBookmark *bm = i.ptr;
-            bm->order = index_PtrArrayConstIterator(&i) + 1;
-        }
+        /* Old format has an implicit alphabetic sort order. */
+        sort_Bookmarks(d, 0, cmpTitleAscending_Bookmark);
         return;
     }
     iBookmarkLoader loader;
@@ -317,7 +327,9 @@ uint32_t add_Bookmarks(iBookmarks *d, const iString *url, const iString *title,
                        iChar icon) {
     lock_Mutex(d->mtx);
     iBookmark *bm = new_Bookmark();
-    set_String(&bm->url, canonicalUrl_String(url));
+    if (url) {
+        set_String(&bm->url, canonicalUrl_String(url));
+    }
     set_String(&bm->title, title);
     if (tags) {
         set_String(&bm->tags, tags);
@@ -471,7 +483,7 @@ const iString *bookmarkListPage_Bookmarks(const iBookmarks *d, enum iBookmarkLis
     const iPtrArray *bmList = list_Bookmarks(d,
                                              listType == listByCreationTime_BookmarkListType
                                                  ? cmpTimeDescending_Bookmark_
-                                                 : cmpTitleAscending_Bookmark_,
+                                                 : cmpTitleAscending_Bookmark,
                                              NULL,
                                              NULL);
     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 {
     int order;         /* sort order */
 };
 
-iLocalDef uint32_t  id_Bookmark (const iBookmark *d) { return d->node.key; }
+iLocalDef uint32_t  id_Bookmark         (const iBookmark *d) { return d->node.key; }
+iLocalDef iBool     isFolder_Bookmark   (const iBookmark *d) { return isEmpty_String(&d->url); }
 
 iBool   hasTag_Bookmark     (const iBookmark *, const char *tag);
 void    addTag_Bookmark     (iBookmark *, const char *tag);
@@ -73,11 +74,17 @@ iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add)
     }
 }
 
+int     cmpTitleAscending_Bookmark      (const iBookmark **, const iBookmark **);
+int     cmpTree_Bookmark                (const iBookmark **, const iBookmark **);
+
 /*----------------------------------------------------------------------------------------------*/
 
 iDeclareType(Bookmarks)
 iDeclareTypeConstruction(Bookmarks)
 
+typedef iBool (*iBookmarksFilterFunc)   (void *context, const iBookmark *);
+typedef int   (*iBookmarksCompareFunc)  (const iBookmark **, const iBookmark **);
+
 void        clear_Bookmarks             (iBookmarks *);
 void        load_Bookmarks              (iBookmarks *, const char *dirPath);
 void        save_Bookmarks              (const iBookmarks *, const char *dirPath);
@@ -88,15 +95,13 @@ iBool       remove_Bookmarks            (iBookmarks *, uint32_t id);
 iBookmark * get_Bookmarks               (iBookmarks *, uint32_t id);
 void        reorder_Bookmarks           (iBookmarks *, uint32_t id, int newOrder);
 iBool       updateBookmarkIcon_Bookmarks(iBookmarks *, const iString *url, iChar icon);
+void        sort_Bookmarks              (iBookmarks *, uint32_t parentId, iBookmarksCompareFunc cmp);
 void        fetchRemote_Bookmarks       (iBookmarks *);
 void        requestFinished_Bookmarks   (iBookmarks *, iGmRequest *req);
 
 iChar       siteIcon_Bookmarks          (const iBookmarks *, const iString *url);
 uint32_t    findUrl_Bookmarks           (const iBookmarks *, const iString *url); /* O(n) */
 
-typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *);
-typedef int   (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **);
-
 iBool   filterTagsRegExp_Bookmarks      (void *regExp, const iBookmark *);
 
 /**
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) {
 #define rightArrow_Icon     "\u279e"
 #define barLeftArrow_Icon   "\u21a4"
 #define barRightArrow_Icon  "\u21a6"
+#define upDownArrow_Icon    "\u21c5"
 #define clock_Icon          "\U0001f553"
 #define pin_Icon            "\U0001f588"
 #define star_Icon           "\u2605"
@@ -155,6 +156,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
 #define return_Icon         "\u23ce"
 #define undo_Icon           "\u23ea"
 #define select_Icon         "\u2b1a"
+#define downAngle_Icon      "\ufe40"
 
 #if defined (iPlatformApple)
 #   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_[] =
     { 46, { "${keys.link.homerow.hover}",   'h', 0,                         "document.linkkeys arg:1 hover:1"   }, 0 },
     { 47, { "${keys.link.homerow.next}",    '.', 0,                         "document.linkkeys more:1"          }, 0 },
     { 50, { "${keys.bookmark.add}",         'd', KMOD_PRIMARY,              "bookmark.add"                      }, 0 },
+    { 51, { "${keys.bookmark.addfolder}",   'n', KMOD_SHIFT,                "bookmarks.addfolder"               }, 0 },
     { 55, { "${keys.subscribe}",            subscribeToPage_KeyModifier,    "feeds.subscribe"                   }, 0 },
     { 60, { "${keys.findtext}",             'f', KMOD_PRIMARY,              "focus.set id:find.input"           }, 0 },
     { 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
     const iRect   rect = itemRect_ListWidget(d, index);
     const iRangei span = ySpan_Rect(rect);
     if (item->isDropTarget) {
-        const int pad = size_Range(&span) / 4;
-        if (dstPos.y >= span.start + pad && dstPos.y < span.end) {
+        const int pad = size_Range(&span) / 3;
+        if (dstPos.y >= span.start + pad && dstPos.y < span.end - pad) {
             *isOnto = iTrue;
             return index;
         }
@@ -352,11 +352,13 @@ static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) {
     stop_Anim(&d->scrollY.pos);
     iBool isOnto;
     const size_t index = resolveDragDestination_ListWidget_(d, endPos, &isOnto);
-    if (isOnto) {
-        postCommand_Widget(d, "list.dragged arg:%zu onto:%zu", d->dragItem, index);
-    }
-    else if (index != d->dragItem) {
-        postCommand_Widget(d, "list.dragged arg:%zu before:%zu", d->dragItem, index);
+    if (index != d->dragItem) {
+        if (isOnto) {
+            postCommand_Widget(d, "list.dragged arg:%zu onto:%zu", d->dragItem, index);
+        }
+        else {
+            postCommand_Widget(d, "list.dragged arg:%zu before:%zu", d->dragItem, index);
+        }
     }
     invalidateItem_ListWidget(d, d->dragItem);
     d->dragItem = iInvalidPos;
@@ -394,7 +396,7 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
         }
         else if (d->dragItem != iInvalidPos) {
             /* Start scrolling if near the ends. */
-            const int zone = d->itemHeight;
+            const int zone = 2 * d->itemHeight;
             const iRect bounds = bounds_Widget(w);
             float scrollSpeed = 0.0f;
             if (mousePos.y > bottom_Rect(bounds) - zone) {
@@ -410,7 +412,7 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
             }
             else {
                 setValueSpeed_Anim(&d->scrollY.pos, scrollSpeed < 0 ? 0 : scrollMax_ListWidget_(d),
-                                   iAbs(scrollSpeed * gap_UI * 100));
+                                   scrollSpeed * scrollSpeed * gap_UI * 400);
                 refreshWhileScrolling_ListWidget_(d);
             }
         }
@@ -451,7 +453,7 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
                     ((const iListItem *) item_ListWidget(d, over))->isDraggable) {
                     d->dragItem = over;
                     d->dragOrigin = sub_I2(topLeft_Rect(itemRect_ListWidget(d, over)),
-                                           pos_Click(&d->click));
+                                           d->click.startPos);
                     invalidateItem_ListWidget(d, d->dragItem);
                 }
             }
@@ -569,20 +571,22 @@ static void draw_ListWidget_(const iListWidget *d) {
         SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
         iBool dstOnto;
         const size_t dstIndex = resolveDragDestination_ListWidget_(d, mousePos, &dstOnto);
-        if (dstIndex != d->dragItem && dstIndex != d->dragItem + 1) {
+        if (dstIndex != d->dragItem) {
             const iRect dstRect = itemRect_ListWidget(d, dstIndex);
             p.alpha = 0xff;
             if (dstOnto) {
-                fillRect_Paint(&p, dstRect, uiTextAction_ColorId);
+                drawRectThickness_Paint(&p, dstRect, gap_UI / 2, uiTextAction_ColorId);
             }
-            else {
+            else if (dstIndex != d->dragItem + 1) {
                 fillRect_Paint(&p, (iRect){ addY_I2(dstRect.pos, -gap_UI / 4),
                                             init_I2(width_Rect(dstRect), gap_UI / 2) },
                                uiTextAction_ColorId);
             }
         }
         p.alpha = 0x80;
+        setOpacity_Text(0.5f);
         class_ListItem(item)->draw(item, &p, itemRect, d);
+        setOpacity_Text(1.0f);
         SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
     }
     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) {
 }
 
 static float bookmarkRelevance_LookupJob_(const iLookupJob *d, const iBookmark *bm) {
+    if (isFolder_Bookmark(bm)) {
+        return 0.0f;
+    }
     iUrl parts;
     init_Url(&parts, &bm->url);
     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 {
     iWidget *         menu;
     iSidebarItem *    contextItem;  /* list item accessed in the context menu */
     size_t            contextIndex; /* index of list item accessed in the context menu */
+    iIntSet           closedFolders; /* otherwise open */
 };
 
 iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side)
@@ -116,26 +117,67 @@ static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) {
     return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0;
 }
 
-static int cmpTree_Bookmark_(const iBookmark **a, const iBookmark **b) {
+iBookmark *parent_Bookmark(const iBookmark *d) {
+    /* TODO: Parent pointers should be prefetched! */
+    if (d->parentId) {
+        return get_Bookmarks(bookmarks_App(), d->parentId);
+    }
+    return NULL;
+}
+
+iBool hasParent_Bookmark(const iBookmark *d, uint32_t parentId) {
+    /* TODO: Parent pointers should be prefetched! */
+    while (d->parentId) {
+        if (d->parentId == parentId) {
+            return iTrue;
+        }
+        d = get_Bookmarks(bookmarks_App(), d->parentId);
+    }
+    return iFalse;
+}
+
+int depth_Bookmark(const iBookmark *d) {
+    /* TODO: Precalculate this! */
+    int depth = 0;
+    for (; d->parentId; depth++) {
+        d = get_Bookmarks(bookmarks_App(), d->parentId);
+    }
+    return depth;
+}
+
+int cmpTree_Bookmark(const iBookmark **a, const iBookmark **b) {
     const iBookmark *bm1 = *a, *bm2 = *b;
-    if (bm2->parentId == id_Bookmark(bm1)) {
+    /* Contents of a parent come after it. */
+    if (hasParent_Bookmark(bm2, id_Bookmark(bm1))) {
         return -1;
     }
-    if (bm1->parentId == id_Bookmark(bm2)) {
+    if (hasParent_Bookmark(bm1, id_Bookmark(bm2))) {
         return 1;
     }
-    if (bm1->parentId == bm2->parentId) {
-        //return cmpStringCase_String(&bm1->title, &bm2->title);
-        return iCmp(bm1->order, bm2->order);
-    }
-    if (bm1->parentId) {
-        bm1 = get_Bookmarks(bookmarks_App(), bm1->parentId);
-    }
-    if (bm2->parentId) {
-        bm2 = get_Bookmarks(bookmarks_App(), bm2->parentId);
+    /* Comparisons are only valid inside the same parent. */
+    while (bm1->parentId != bm2->parentId) {
+        int depth1 = depth_Bookmark(bm1);
+        int depth2 = depth_Bookmark(bm2);
+        if (depth1 != depth2) {
+            /* Equalize the depth. */
+            while (depth1 > depth2) {
+                bm1 = parent_Bookmark(bm1);
+                depth1--;
+            }
+            while (depth2 > depth1) {
+                bm2 = parent_Bookmark(bm2);
+                depth2--;
+            }
+            continue;
+        }
+        bm1 = parent_Bookmark(bm1);
+        depth1--;
+        bm2 = parent_Bookmark(bm2);
+        depth2--;
     }
-//    return cmpStringCase_String(&bm1->title, &bm2->title);
-    return iCmp(bm1->order, bm2->order);
+    const int cmp = iCmp(bm1->order, bm2->order);
+    if (cmp) return cmp;
+    return cmpStringCase_String(&bm1->title, &bm2->title);
 }
 
 static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const char *label,
@@ -211,6 +253,16 @@ static void updateContextMenu_SidebarWidget_(iSidebarWidget *d) {
     d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));    
 }
 
+static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBookmark *bm) {
+    while (bm->parentId) {
+        if (contains_IntSet(&d->closedFolders, bm->parentId)) {
+            return iTrue;
+        }
+        bm = get_Bookmarks(bookmarks_App(), bm->parentId);
+    }
+    return iFalse;
+}
+
 static void updateItems_SidebarWidget_(iSidebarWidget *d) {
     clear_ListWidget(d->list);
     releaseChildren_Widget(d->blank);
@@ -334,12 +386,22 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
             iRegExp *remoteSourceTag = iClob(new_RegExp("\\b" remoteSource_BookmarkTag "\\b", caseSensitive_RegExpOption));
             iRegExp *remoteTag       = iClob(new_RegExp("\\b" remote_BookmarkTag "\\b", caseSensitive_RegExpOption));
             iRegExp *linkSplitTag    = iClob(new_RegExp("\\b" linkSplit_BookmarkTag "\\b", caseSensitive_RegExpOption));
-            iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark_, NULL, NULL)) {
+            iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark, NULL, NULL)) {
                 const iBookmark *bm = i.ptr;
+                if (isBookmarkFolded_SidebarWidget_(d, bm)) {
+                    continue; /* inside a closed folder */
+                }
                 iSidebarItem *item = new_SidebarItem();
                 item->listItem.isDraggable = iTrue;
+                item->isBold = item->listItem.isDropTarget = isFolder_Bookmark(bm);
                 item->id = id_Bookmark(bm);
-                item->icon = bm->icon;
+                item->indent = depth_Bookmark(bm);
+                if (isFolder_Bookmark(bm)) {
+                    item->icon = contains_IntSet(&d->closedFolders, item->id) ? 0x27e9 : 0xfe40;
+                }
+                else {
+                    item->icon = bm->icon;
+                }
                 set_String(&item->url, &bm->url);
                 set_String(&item->label, &bm->title);
                 /* Icons for special tags. */ {
@@ -384,8 +446,11 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
                                { "---", 0, 0, NULL },
                                { delete_Icon " " uiTextCaution_ColorEscape "${bookmark.delete}", 0, 0, "bookmark.delete" },
                                { "---", 0, 0, NULL },
-                               { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } },
-               14);
+                               { add_Icon " ${menu.newfolder}", 0, 0, "bookmarks.addfolder" },
+                               { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" },
+                               { "---", 0, 0, NULL },
+                               { upDownArrow_Icon " ${menu.sort.alpha}", 0, 0, "bookmark.sortfolder" } },
+               17);
             break;
         }
         case history_SidebarMode: {
@@ -671,6 +736,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
     d->resizer = NULL;
     d->list = NULL;
     d->actions = NULL;
+    init_IntSet(&d->closedFolders);
     /* On a phone, the right sidebar is used exclusively for Identities. */
     const iBool isPhone = deviceType_App() == phone_AppDeviceType;
     if (!isPhone || d->side == left_SidebarSide) {
@@ -754,6 +820,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) {
 
 void deinit_SidebarWidget(iSidebarWidget *d) {
     deinit_String(&d->cmdPrefix);
+    deinit_IntSet(&d->closedFolders);
 }
 
 iBool setButtonFont_SidebarWidget(iSidebarWidget *d, int font) {
@@ -801,6 +868,17 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
             break;
         }
         case bookmarks_SidebarMode:
+            if (isEmpty_String(&item->url)) /* a folder */ {
+                if (contains_IntSet(&d->closedFolders, item->id)) {
+                    remove_IntSet(&d->closedFolders, item->id);
+                }
+                else {
+                    insert_IntSet(&d->closedFolders, item->id);
+                }
+                updateItems_SidebarWidget_(d);
+                break;
+            }
+            /* fall through */
         case history_SidebarMode: {
             if (!isEmpty_String(&item->url)) {
                 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
                                                      isLast ? numItems_ListWidget(d->list) - 1
                                                             : beforeIndex);
     const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id);
+    if (hasParent_Bookmark(dst, movingItem->id)) {
+        return;
+    }
     reorder_Bookmarks(bookmarks_App(), movingItem->id, dst->order + (isLast ? 1 : 0));
+    get_Bookmarks(bookmarks_App(), movingItem->id)->parentId = dst->parentId;
     updateItems_SidebarWidget_(d);
     /* Don't confuse the user: keep the dragged item in hover state. */
     setHoverItem_ListWidget(d->list, index < beforeIndex ? beforeIndex - 1 : beforeIndex);
+    postCommandf_App("bookmarks.changed nosidebar:%p", d); /* skip this sidebar since we updated already */
+}
+
+static void bookmarkMovedOntoFolder_SidebarWidget_(iSidebarWidget *d, size_t index,
+                                                   size_t folderIndex) {
+    const iSidebarItem *movingItem = item_ListWidget(d->list, index);
+    const iSidebarItem *dstItem    = item_ListWidget(d->list, folderIndex);
+    iBookmark *bm = get_Bookmarks(bookmarks_App(), movingItem->id);
+    bm->parentId = dstItem->id;
+    postCommand_App("bookmarks.changed");
 }
 
 static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) {
@@ -1070,7 +1162,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
         }
         else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode ||
                                                              d->mode == feeds_SidebarMode)) {
-            updateItems_SidebarWidget_(d);
+            if (pointerLabel_Command(cmd, "nosidebar") != d) {
+                updateItems_SidebarWidget_(d);
+            }
         }
         else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) {
             updateItems_SidebarWidget_(d);
@@ -1135,6 +1229,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
             }
             else {
                 /* Dragged onto a folder. */
+                bookmarkMovedOntoFolder_SidebarWidget_(d,
+                                                       argU32Label_Command(cmd, "arg"),
+                                                       argU32Label_Command(cmd, "onto"));
             }
             return iTrue;
         }
@@ -1170,15 +1267,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
                     setText_InputWidget(findChild_Widget(dlg, "bmed.icon"),
                                         collect_String(newUnicodeN_String(&bm->icon, 1)));
                 }
-                setFlags_Widget(findChild_Widget(dlg, "bmed.tag.home"),
-                                selected_WidgetFlag,
-                                hasTag_Bookmark(bm, homepage_BookmarkTag));
-                setFlags_Widget(findChild_Widget(dlg, "bmed.tag.remote"),
-                                selected_WidgetFlag,
-                                hasTag_Bookmark(bm, remoteSource_BookmarkTag));
-                setFlags_Widget(findChild_Widget(dlg, "bmed.tag.linksplit"),
-                                selected_WidgetFlag,
-                                hasTag_Bookmark(bm, linkSplit_BookmarkTag));
+                setToggle_Widget(findChild_Widget(dlg, "bmed.tag.home"),
+                                 hasTag_Bookmark(bm, homepage_BookmarkTag));
+                setToggle_Widget(findChild_Widget(dlg, "bmed.tag.remote"),
+                                 hasTag_Bookmark(bm, remoteSource_BookmarkTag));
+                setToggle_Widget(findChild_Widget(dlg, "bmed.tag.linksplit"),
+                                 hasTag_Bookmark(bm, linkSplit_BookmarkTag));
                 setCommandHandler_Widget(dlg, handleBookmarkEditorCommands_SidebarWidget_);
                 setFocus_Widget(findChild_Widget(dlg, "bmed.title"));
             }
@@ -1225,6 +1319,16 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
             }
             return iTrue;
         }
+        else if (isCommand_Widget(w, ev, "bookmark.sortfolder")) {
+            const iSidebarItem *item = d->contextItem;
+            if (d->mode == bookmarks_SidebarMode && item) {
+                postCommandf_App("bookmarks.sort arg:%zu",
+                                 item->listItem.isDropTarget
+                                     ? item->id
+                                     : get_Bookmarks(bookmarks_App(), item->id)->parentId);
+            }
+            return iTrue;
+        }
         else if (equal_Command(cmd, "feeds.update.finished")) {
             d->numUnreadEntries = argLabel_Command(cmd, "unread");
             checkModeButtonLayout_SidebarWidget_(d);
@@ -1638,7 +1742,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
         fillRect_Paint(p, itemRect, bg);
     }
     else if (sidebar->mode == bookmarks_SidebarMode) {
-        if (d->icon == 0x2913) { /* TODO: Remote icon; meaning: is this in a folder? */
+        if (d->indent) /* remote icon */  {
             bg = uiBackgroundFolder_ColorId;
             fillRect_Paint(p, itemRect, bg);
         }
@@ -1740,11 +1844,13 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
     }
     else if (sidebar->mode == bookmarks_SidebarMode) {
         const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
-                               : uiText_ColorId;
+            : d->listItem.isDropTarget ? uiHeading_ColorId : uiText_ColorId;
+        /* The icon. */
         iString str;
         init_String(&str);
-        appendChar_String(&str, d->icon ? d->icon : 0x1f588);        
-        const iRect iconArea = { addX_I2(pos, gap_UI),
+        appendChar_String(&str, d->icon ? d->icon : 0x1f588);
+        const int leftIndent = d->indent * gap_UI * 4;
+        const iRect iconArea = { addX_I2(pos, gap_UI + leftIndent),
                                  init_I2(1.75f * lineHeight_Text(font), itemHeight) };
         drawCentered_Text(font,
                           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) {
     setUserData_Object(menu, deepCopyMenuItems_(menu, items, n));
     addChild_Widget(parent, menu);
     iRelease(menu); /* owned by parent now */
+    /* Keyboard shortcuts still need to triggerable via the menu, although the items don't exist. */ {
+        for (size_t i = 0; i < n; i++) {
+            const iMenuItem *item = &items[i];
+            if (item->key) {
+                addAction_Widget(menu, item->key, item->kmods, item->command);
+            }
+        }
+    }
 #else
     /* Non-native custom popup menu. This may still be displayed inside a separate window. */
     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_[] = {
 static iMenuItem bookmarksMenuItems_[] = {
     { "${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
     { "${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" },
+    { "${menu.newfolder}", 0, 0, "bookmarks.addfolder" },
     { "---", 0, 0, NULL },
     { "${menu.import.links}", 0, 0, "bookmark.links confirm:1" },
     { "---", 0, 0, NULL },
@@ -128,6 +129,8 @@ static iMenuItem bookmarksMenuItems_[] = {
     { "${macos.menu.bookmarks.bytime}", 0, 0, "open url:about:bookmarks?created" },
     { "${menu.feeds.entrylist}", 0, 0, "open url:about:feeds" },
     { "---", 0, 0, NULL },
+    { "${menu.sort.alpha}", 0, 0, "bookmarks.sort" },
+    { "---", 0, 0, NULL },
     { "${menu.bookmarks.refresh}", 0, 0, "bookmarks.reload.remote" },
     { "${menu.feeds.refresh}", SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh" },
 };
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.7/cdiff/53f0596cfd6060cd63cc8e6f92f981672da97ee2
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
89.24371 milliseconds
Gemini-to-HTML Time
1.528124 milliseconds

This content has been proxied by September (ba2dc).