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